JAVA代码审计

32 阅读6分钟

靶场搭建

靶场地址:

j3ers3/Hello-Java-Sec: ☕️ Java Security,安全编码和代码审计

搭建靶场的时候,可以删除db.sql文件第8行的DEFAULT (UUID()) UNIQUE,mysql 5.7.4 不支持该语法。

其他靶场:

JoyChou93/java-sec-code: Java web common vulnerabilities and security code which is base on springboot and spring security

whgojp/JavaSecLab: JavaSecLab is a comprehensive Java vulnerability platform| JavaSecLab是一款综合型Java漏洞平台,提供相关漏洞缺陷代码、修复代码、漏洞场景、审计SINK点、安全编码规范,覆盖多种漏洞场景,友好用户交互UI……

1. 修改配置文件

src/main/resources/application.properties 文件显示spring.profiles.active=dev,说明启用了 dev 这个配置文件。

image-20251230173648877.png

修改配置文件 src\main\resources\application-dev.properties

image-20251229230449554.png

2. 导入数据库

找到 src/main/resources/db.sql 数据库文件,打开 Navicat 新建查询然后粘贴运行。

image-20251230174150520.png

3. 修改镜像源(可选)

找到依赖文件 pom.xml,右键选择Maven,点击创建 settings.xml。

image-20251230174337374.png

插入国内镜像源:

<mirrors>
   <mirror>
       <id>aliyun</id>
       <mirrorOf>central</mirrorOf>
       <name>Aliyun Maven</name>
       <url>https://maven.aliyun.com/repository/public</url>
   </mirror>
</mirrors>

image-20251230174925083.png

4. 启动网站

找到入口函数:src/main/java/com/best/hello/HelloApplication.java

image-20251230194146522.png

点击绿色三角形。

image-20251230175008596.png

默认网站端口是8888

默认账号:admin/admin

image-20251230175329037.png

sql注入漏洞

java常见的连接数据库的方式有以下几种:

  • 原生JDBC

  • MyBatis

  • Hibernate

  • JDBCtemplate

原生JDBC

Java 数据库连接(Java Database Connectivity,JDBC)是 Java 语言用于执行与数据库交互的标准 API。它允许 Java 应用程序与各种关系型数据库进行通信,包括但不限于 MySQL、PostgreSQL、 Oracle、SQL Server 等。

JDBC 主要由以下几个关键组件组成:

  • DriverManager:这是 JDBC 的入口点,用于获取数据库连接。
  • Driver:数据库驱动程序是 JDBC 的核心组件之一。
  • Connection:一旦驱动程序被注册,开发人员可以使用 DriverManager 来获取数据库连接。
  • Statement:Statement 对象用于执行静态 SQL 语句并返回它所生成的结果。
  • PreparedStatement:PreparedStatement 对象表示预编译的 SQL 语句
  • ResultSet:当执行查询语句时,会返回一个 ResultSet 对象,它包含了查询结果的数据。

Statement 注入

JDBC有两种方法执行SQL语句,分别为PrepareStatement和Statement。

PrepareStatement会对SQL语句进行预编译,而Statement方法在每次执行时都需要编译

Statement 漏洞代码样例:

String sql = "select * from user where id ="+req.getParameter("id");// 拼接sql语句

PrintWriter out = resp.getWriter();
out.println("Statement Demo");
out.println("SQL: "+sql);
try {
	Statement st = conn.createStatement();
	ResultSet rs = st.executeQuery(sql);// 直接执行拼接好的sql语句
	while (rs.next()){
		out.println("<br>Result: "+ rs.getObject("name"));
	}
} catch (SQLException throwables) {
	throwables.printStackTrace();
}

PrepareStatement注入

PrepareStatement方法支持使用 ? 对变量位进行占位,在预编译阶段填入相应的值构造出完整的SQL语句,此时可以避免SQL注入的产生。但开发者有时为了便利,会直接采取拼接的方式构造SQL语句,此时进行预编译则无法阻止SQL注入的产生。

PrepareStatement漏洞代码样例 :

String sql = "select * from user where id ="+req.getParameter("id");// 直接拼接

PrintWriter out = resp.getWriter();
out.println("prepareStatement Demo");
out.println("SQL: "+sql);
try {
	PreparedStatement pst = conn.prepareStatement(sql);// 预编译发生在拼接之后,无法阻止SQL注入的产生。
	ResultSet rs = pst.executeQuery();
	while (rs.next()){
	out.println("<br>Result: "+ rs.getObject("name"));
	}
} catch (SQLException throwables) {
	throwables.printStackTrace();
}

安全写法:

String sql = "select * from user where id = ?";// ?作为占位符
out.println(sql);
try {
	PreparedStatement pstt = conn.prepareStatement(sql);
	// 参数已经强制要求是整型
	pstt.setInt(1, Integer.parseInt(req.getParameter("id")));// 传入参数
	ResultSet rs = pstt.executeQuery();
	while (rs.next()){
	out.println("<br> id: "+rs.getObject("id"));
	out.println("<br> name: "+rs.getObject("name"));
}
} catch (SQLException throwables) {
	throwables.printStackTrace();
}

(靶场有示例)

image-20251230201833530.png

MyBatis

JDBC方式是将SQL语句写在代码块中,不利于后续维护。如今的Java项目或多或少会使用对JDBC进行更抽象封装的持久化框架,如MyBatis和Hibernate。通常,框架底层已经实现了对SQL注入的防御,但在研发人员未能恰当使用框架的情况下,仍然可能存在SQL注入的风险。

介绍

MyBatis 是一个流行的持久层框架,它提供了一种优雅而简单的方式来管理数据库访问。 MyBatis 的设计目标是使 SQL 映射更加简单,同时保留 SQL 的灵活性和性能。

MyBatis 允许开发人员使用 XML 文件或者注解来定义 SQL 语句和映射规则,将数据库表中的数据映射到 Java 对象上。

使用示例

XML 配置示例:

<!-- UserMapper.xml -->
<mapper namespace="com.example.UserMapper">
<select id="getUserById" resultType="User">
	SELECT * FROM users WHERE id = #{userId} 
</select>
</mapper>

( sql 语句在XML文件里,# 写法是安全的)

Java 代码示例:

// UserMapper.java(java类)
public interface UserMapper {
	User getUserById(int userId);
}
// 使用 MyBatis 查询用户
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUserById(123);
sqlSession.close(); // 无sql语句

mybatis注入

  • 直接拼接 ]
<select id="QueryByName" parameterType="String" resultType="com.demo.bean.User">
select * from user where name = ${name}
</select>

(用 $ 直接拼接,可以直接搜索)

image-20251230202953877.png

注意:只有string类型才存在sql注入。

(在导航中选择调用层次结构,可以看到接口的实现)

image-20251230205642931.png

image-20251230211644251.png

(@GetMapping 就是存在接口,接口可以导致sql注入)

image-20251230211621285.png

  • orderby 注入
<select id="orderbyInjection" parameterType="String" resultMap="User">
	select * from user order by ${sort} asc
</select>
  • like 注入
<select id="likeInjection" parameterType="String" resultMap="User">
select * from user where username like '%${username}%'
</select>
  • in 注入
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
	SELECT *
		FROM user
	WHERE name IN
		<foreach collection="names" item="name" open="(" close=")" separator=",">
			${name}
		</foreach>
</select>

Hibernate

Hibernate 是一个流行的开源对象关系映射(ORM)框架,它提供了一种优雅而高效的方式来将对象模型和关系型数据库之间进行映射。

Hibernate 允许开发人员将 Java 对象和关系型数据库表之间进行映射,使得数据库操作更加面向对象化。

使用示例

  • 实体类示例
@Entity
@Table(name = "user")  // 关联user表,两个@后面都是关键字
public class User {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String username;
	private String email;
	// 省略其他属性和方法
}
  • 数据访问示例
// 保存用户
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

User user = new User();
user.setUsername("john_doe");
user.setEmail("john@example.com");

session.save(user);   // 插入数据
tx.commit();
session.close();

// 查询用户
Session session = sessionFactory.openSession();
User user = session.get(User.class, 123);
session.close();

Hibernate注入

在 Hibernate 中, HQL 查询中拼接字符串可能导致 SQL 注入漏洞。

String userInput = "someUserInput";
Query query = session.createQuery("FROM User WHERE username = '" + userInput +
"'");  // 出现了一些sql语句和 +号拼接
List<User> users = query.list();

解决方案

使用参数化查询来代替直接拼接字符串。

String userInput = "someUserInput";
Query query = session.createQuery("FROM User WHERE username = :username"); // :号绑定参数
query.setParameter("username", userInput);
List<User> users = query.list();

JdbcTemplate

JdbcTemplate 封装了大量的 JDBC 操作,使得开发人员能够更专注于业务逻辑的实现,而不必处理繁琐的 JDBC 细节。

JdbcTemplate 封装了 JDBC 的大部分操作,使得开发人员无需编写繁琐的连接管理、 Statement 和 ResultSet 处理等代码。

使用示例

  • 查询示例
public class UserDao {
	private JdbcTemplate jdbcTemplate;

	public void setDataSource(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	} 
    
	public User getUserById(Long userId) {
		return jdbcTemplate.queryForObject(
			"SELECT * FROM users WHERE id = ?", // ?号绑定
			new Object[]{userId},
			(rs, rowNum) -> {
				User user = new User();
				user.setId(rs.getLong("id"));
				user.setUsername(rs.getString("username"));
				user.setEmail(rs.getString("email"));
				return user;
			}
		);
	}
}
  • 更新示例
public class UserDao {
	private JdbcTemplate jdbcTemplate;
	public void setDataSource(DataSource dataSource) {
		this.jdbcTemplate = new JdbcTemplate(dataSource);
	} 
	public void updateUserEmail(Long userId, String newEmail) {
		jdbcTemplate.update(
		"UPDATE users SET email = ? WHERE id = ?",
		newEmail, userId
		);
	}
}

JdbcTemplate注入

@RequestMapping(method = RequestMethod.GET)
public @ResponseBody
List<Customer> getCustomerByid(@RequestParam String id) {
	String sql = "SELECT * FROM customers WHERE id = " +id;  // 使用了 +号,且是string类型
	return jdbcTemplate.query(sql, new CustomerMapper());
}

XSS 漏洞

直接在 html 页面展示“用户可控数据”,将直接导致跨站脚本威胁。

Java 示例: JSP 文件

代码示例

<%
String normal_querystring = "?input=reports";
String input = request.getParameter("input");

if (input != null) {
	try {
		out.println(input);  // 直接输出用户输入的变量
	} catch (Exception e) {
		out.print("<pre>");
		e.printStackTrace(response.getWriter());
		out.print("</pre>");
	}
} 
else {
%>

代码中的几个变量被直接输出到了页面中,没有做任何安全过滤,一旦让用户可以输入数据,都可能导致用户浏览器把“用户可控数据”当成JS脚本执行,或页面元素被“用户可控数据”插入的页面 HTML 代码控制,从而造成攻击。

解决方案

  1. 在 HTML/XML 中显示“用户可控数据”前,应该进行 html escape 转义
<div>#escapeHTML($user.name) </div>
<td>#escapeHTML($user.name)</td>
  1. 在 javascript 内容中输出的“用户可控数据”,需要做 javascript escape 转义。
<script>alert('#escapeJavaScript($user.name)')</script>
<script>x='#escapeJavaScript($user.name)'</script>
<div onmouseover="x='#escapeJavaScript($user.name)'"</div>
  1. 在给用户设置认证 COOKIE 时,加入 HTTPONLY

命令注入漏洞

安全威胁

我们平时讲的RCE漏洞, R是指Remote, C可以指代Code也可以指Command, E是指Execution,所以远程命令执行(Remote Command Execution)也是RCE的一种。 pre_auth rce, 未授权远程命令执行

代码示例

ProcessBuilder

java.lang.ProcessBuilder 中 start() 方法可以执行系统命令,命令和参数可以通过构造方法的String List 或 String 数组来传入。

通过 ProcessBuilder 来执行任意命令,需要代码中创建 shell 来执行命令,并且参数可控或存在拼接

  • 漏洞代码示例1
// String dir = "xxxx";
String[] cmdList = new String[]{"sh", "-c", "ls -lh " + dir}; // ; | & `` $()
ProcessBuilder builder = new ProcessBuilder(cmdList);
builder.redirectErrorStream(true);
Process process = builder.start(); // 使用了start方法,参数dir可控制,就会产生漏洞。
printOutput(process.getInputStream());

Runtime

java.lang.Runtime 中 exec() 函数同样可以执行系统命令,命令参数支持 String 和 String 数组两种方式

使用方法:

exec(String command)
exec(String[] cmdarray)
exec(String command, String[] envp)
exec(String command, String[] envp, File dir)
exec(String[] cmdarray, String[] envp, File dir)

代码示例:

@RequestMapping("/runtime")
public static String cmd2(String cmd) { // 传入cmd参数
StringBuilder sb = new StringBuilder();
	try {
		Process proc = Runtime.getRuntime().exec(cmd); // 直接调用.exec执行cmd命令
		InputStream fis = proc.getInputStream();
		InputStreamReader isr = new InputStreamReader(fis);
		BufferedReader br = new BufferedReader(isr);

补充:IDEA的在文件中查找功能(编辑|查找|在路径中查找)。

image-20260101220008195.png

总结:

  • 如果参数完全可控,则可以执行任意命令

  • 若没有手动创建 shell 执行命令,没有存在参数注入,则无法实现命令注入

  • 手动创建 shell 执行命令,可执行-c 的参数值的命令,但注入的命令内不能有空格、 \t\n\r\f 分隔符,否则会被分割

// 使用 ${IFS} (对应内部字段分隔符) 来代替空格, 成功执行
String cmd = "sh -c curl${IFS}example.com";

image-20260101221707006.png

解决方案

  • 应尽量避免使用 Runtime 和 ProcessBuilder 来执行系统命令,可搜索系统是否提供 API 来完成同样的功能,如执行删除文件 rm /home/www/log.txt 的命令,可以使用 File.delete() 等函数来代替
  • 无法避免执行命令时,应当尽可能避免创建 shell 来执行系统命令,优先使用 Runtime 和ProcessBuilder 的 字符串数组String[] cmdarray 的 方法,可一定程度上降低命令注入的产生
  • 最后,可考虑使用白名单的方式,限制可执行的命令和允许的参数值,或限制用户输入的所允许字符,如只允许字母数组、下划线

代码执行漏洞

Java的javax.script.ScriptEngineManager类的eval函数可以被控制用于执行代码,幸运的是,在Java 8之后ScriptEngineManager的eval函数就没有了。

代码示例

@GetMapping("/js")
public void jsEngine(String url) throws Exception {
	ScriptEngine engine = new
ScriptEngineManager().getEngineByName("JavaScript");
	Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
	String payload = String.format("load('%s')", url);  // playload用户可控
	System.out.println(payload);
	engine.eval(payload, bindings);  // 使用了危险的eval函数

(可执行的代码类型其实是javastript语言,并非java代码)

表达式注入漏洞

表达式注入漏洞类似远程代码执行漏洞,但远程代码执行漏洞是直接注入代码原生语句,而表达式注入漏洞是基于代码之上构建的执行逻辑。

Java语言方面的表达式EL(Expression Language),主要包括一下内容:

  • SpEL表达式(Spring)
  • OGNL表达式(Struts2、 Confluence)
  • Groovy表达式

SpEL 表达式注入

Spring Expression Language(简称 SpEL)是一种功能强大的表达式语言、用于在运行时查询和操作对象图;语法上类似于 Unified EL,但提供了更多的特性,特别是方法调用和基本字符串模板函数。

漏洞代码示例1

// Payload: T(java.lang.Runtime).getRuntime().exec(%27cmd.exe%20/c%20whoami%27)
@GetMapping("/spel")
public String rce(String ex) {
	ExpressionParser parser = new SpelExpressionParser(); // 用到了表达式
	String result = parser.parseExpression(ex).getValue().toString();
	System.out.println(result);  // 参数可控时会产生漏洞
	return result;
}

OGNL注入漏洞

Struts2框架正是因为滥用OGNL表达式,使之成为了“漏洞之王”。

漏洞代码示例1

import ognl.Ognl;
import ognl.OgnlContext;
public class Test {
public static void main(String[] args) throws Exception {
// 创建一个OGNL上下文对象
OgnlContext context = new OgnlContext();
// getValue()触发
// @[类全名(包括包路径)]@[方法名|值名]
Ognl.getValue("@java.lang.Runtime@getRuntime().exec('calc')", context,
context.getRoot());
// setValue()触发
// Ognl.setValue(Runtime.getRuntime().exec("calc"), context,
context.getRoot());
}
}

SSTI模板注入

服务端接收攻击者的恶意输入以后,未经任何处理就将其作为** Web 应用模板内容**的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。

补充:模板可以被认为是一段固定好格式,等着开发人员或者用户来填充信息的文件。通过这种方法,可以做到逻辑与视图分离,更容易、清楚且相对安全地编写前后端不同的逻辑。

SSTI漏洞原理

服务端接收攻击者的恶意输入以后,未经任何处理就将其作为** Web 应用模板内容**的一部分,模板引擎在进行目标编译渲染的过程中,执行了攻击者插入的可以破坏模板的语句,从而达到攻击者的目的。

Thymeleaf

Thymeleaf 是用于Web和独立环境的现代服务器端Java模板引擎。 Thymeleaf 是 spring boot

的推荐引擎。

  • payload:
http://localhost:8888/SSTI/thymeleaf/?
lang=__$%7BT(java.lang.Runtime).getRuntime().exec('calc')%7D__::.x
  • 漏洞代码示例1
@GetMapping("/path")
	public String path(@RequestParam String lang) {
		return lang ; //template path is tainted
}

image-20260101225315515.png

  • 漏洞代码示例2

    根据spring boot定义,如果controller无返回值,则以GetMapping的路由为视图名称。当然,对于每个http请求来讲,其实就是将请求的url作为视图名称,调用模板引擎去解析。在这种情况下,我们只要可以控制请求的controller的参数,一样可以造成RCE漏洞。

/**
* 将请求的url作为视图名称, 调用模板引擎去解析
* 在这种情况下, 我们只要可以控制请求的controller的参数, 一样可以造成RCE漏洞
* payload: __${T(java.lang.Runtime).getRuntime().exec("open -a
Calculator")}__::.x
*/
@GetMapping("/doc/{document}") // document参数可控
	public void getDocument(@PathVariable String document) {
		System.out.println(document);  
}

image-20260101225822137.png

  • 安全代码示例1

设置redirect重定向,根据spring boot定义,如果名称以 redirect: 开头,则不再调用 ThymeleafView 解析,调用RedirectView 去解析 controller 的返回值

@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
	return "redirect:" + url; //CWE-601, as we can control the hostname inredirect
}
  • 安全代码示例2

如果设置 ResponseBody ,则不再调用模板解析。由于controller的参数被设置为HttpServletResponse, Spring认为它已经处理了HTTP Response,因此不会发生视图名称解析

@
GetMapping("/safe/doc/{document}")
public void getDocument(@PathVariable String document, HttpServletResponse
response) {
	log.info("Retrieving " + document); //FP
}

SSRF 漏洞

SSRF(Server-Side Request Forgery) 服务端请求伪造 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下, SSRF攻击的目标是从外网无法访问的内部系统。正是因为它是由服务端发起的,所以它能够请求到与它相连而与外网隔离的内部系统。

代码示例

  • 漏洞代码1:无任何过滤和转换( URLConnection)
URL url = new URL(url);
URLConnection connection = url.openConnection();
connection.connect();
  • 漏洞代码2:返回对象强转
// 此时只能使用HTTP协议去探测
String url = request.getParameter("url"); //接收url的传参
String htmlContent;
PrintWriter writer = response.getWriter(); //获取响应的打印流对象
URL u = new URL(url); //实例化url的对象  !!用new强转
URLConnection urlConnection = u.openConnection();//打开一个URL连接, 并运行客户端访问资源。
HttpURLConnection httpUrl = (HttpURLConnection) urlConnection;
  • 漏洞代码3:利用ssrf读取文件(InputStream)
// http://127.0.0.1:8080//downloadServlet?url=file:///C:\\1.txt
String url = request.getParameter("url");
int len;
OutputStream outputStream = response.getOutputStream();
byte[] bytes = new byte[1024];
InputStream inputStream = file.openStream();

解决方案

  • 过滤返回信息,验证远程服务器对请求的响应是比较容易的方法。如果web应用是去获取某一种类型的文件。那么在把返回结果展示给用户之前先验证返回的信息是否符合标准。

  • 统一错误信息,避免用户可以根据错误信息来判断远端服务器的端口状态。

  • 限制请求的端口为http常用的端口,比如, 80,443,8080,8090。

  • 黑名单内网ip。避免应用被用来获取获取内网数据,攻击内网。

  • **禁用不需要的协议。**仅仅允许http和https请求。可以防止类似于file:///,ftp:// 等引起的问题。

XXE 漏洞

XXE: XML External Entity 即XML外部实体注入攻击。是由于程序在解析输入的XML数据时,解析了攻击者伪造的外部实体。

当允许引用外部实体时,可构造恶意内容,导致读取任意文件、探测内网端口、攻击内网网站、发起DoS拒绝服务攻击、执行系统命令等。 **Java中的XXE支持 sun.net.www.protocol 里的所有协议: http, https, file, ftp, mailto, jar, netdoc。**一般利用file协议读取文件,利用http协议探测内网。

代码示例

  • 漏洞代码示例1
@PostMapping("/xmlReader/vuln")  // Post请求
public String xmlReaderVuln(HttpServletRequest request) {
	try {
		String body = WebUtils.getRequestBody(request);
		logger.info(body);   // body可控
		XMLReader xmlReader = XMLReaderFactory.createXMLReader();  // 解析xml的类
		xmlReader.parse(new InputSource(new StringReader(body))); // parse(解析) xml
		return "xmlReader xxe vuln code";
	} catch (Exception e) {
		logger.error(e.toString());
		return EXCEPT;
	}
}
  • 安全代码示例1
@RequestMapping(value = "/xmlReader/sec", method = RequestMethod.POST)
public String xmlReaderSec(HttpServletRequest request) {
	try {
		String body = WebUtils.getRequestBody(request);
		logger.info(body);
		XMLReader xmlReader = XMLReaderFactory.createXMLReader();
		// fix code start
		xmlReader.setFeature("http://apache.org/xml/features/disallow-doctypedecl", true);  // 禁用外部实体
		xmlReader.setFeature("http://xml.org/sax/features/external-generalentities", false); // 禁用外部通用实体
		xmlReader.setFeature("http://xml.org/sax/features/external-parameterentities", false); // 禁用外部参数实体
		//fix code end
		xmlReader.parse(new InputSource(new StringReader(body))); WebUtils.getRequestBody(request)  // 获取完整的 XML 请求体,而非直接解析请求参数,避免了参数层面的 XML 注入风险;
	} catch (Exception e) {
		logger.error(e.toString());
		return EXCEPT;
	} 
	return "xmlReader xxe security code";
}

( xmlReader 类会解析xml请求,审计时可以当作关键字)

  • 漏洞代码实例2
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	String FEATURE = null;
// dbf.setExpandEntityReferences无法防止xxe
	dbf.setExpandEntityReferences(false);
	DocumentBuilder documentBuilder = dbf.newDocumentBuilder();
	Document document = documentBuilder.parse(new File("poc.xml"));
  • 安全代码示例2
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
String FEATURE = null;
FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
dbf.setFeature(FEATURE, true);
FEATURE = "http://xml.org/sax/features/external-general-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://xml.org/sax/features/external-parameter-entities";
dbf.setFeature(FEATURE, false);
FEATURE = "http://apache.org/xml/features/nonvalidating/load-externaldtd";
dbf.setFeature(FEATURE, false);
dbf.setXIncludeAware(false);
// dbf.setExpandEntityReferences无法防止xxe
dbf.setExpandEntityReferences(false);
DocumentBuilder documentBuilder = dbf.newDocumentBuilder();

(newDocumentBuilder() 也可作为关键字)

  • 漏洞代码示例3
File file = new File("poc.xml");
SAXBuilder sb = new SAXBuilder();
Document doc = sb.build(file);
  • 安全代码示例3
File file = new File("src/main/java/xxe/poc_bind.xml");
SAXBuilder sb = new SAXBuilder();
sb.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
sb.setFeature("http://xml.org/sax/features/external-general-entities", false);
sb.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
sb.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",
false);
  • 漏洞代码示例4
SAXParserFactory spf = SAXParserFactory.newInstance();
File file = new File("poc.xml");
SAXParser parser = spf.newSAXParser();
parser.parse(file, (HandlerBase) null);
  • 安全代码示例4
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
spf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd",
false);
File file = new File("poc.xml");
SAXParser parser = spf.newSAXParser();
parser.parse(file, (HandlerBase) null);

解决方案

  1. 过滤用户提交的XML数据,数据中有可能会发生危害的关键词(黑名单)

  2. 使用开发语言提供的禁用外部实体的方法

    PHP:

    libxml_disable_entity_loader(true);
    

    JAVA:

    DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
    dbf.setExpandEntityReferences(false);
    .setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);
    .setFeature("http://xml.org/sax/features/external-general-entities",false)
    .setFeature("http://xml.org/sax/features/external-parameter-entities",false);
    

    Python:

    from lxml import etree
    xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))
    
  3. 不允许XML中含有自己定义的DTD

文件上传/下载漏洞

代码示例

读写文件函数(关键字):

getInputStream()/FileOutputStream()
getOriginalFilename()
FileWriter
File

或查找filename相关的关键字进行检索;

处理用户上传文件请求的代码,这段代码没有过滤文件扩展名:

@Controller
public class UploadFile {
	@PostMapping("/upload")
	public String uploadFile(@RequestParam("uploadfile")MultipartFile file){
		//获取文件名 test.jsp
		String filename = file.getOriginalFilename();
		//文件保存路径无过滤
		String path="/var/www/html/magedu/upload";
		File outfile = new File(path + filename);
		try {
			file.transferTo(outfile);  // 保存文件,文件名后缀无过滤
		}catch (IOException e){
			e.printStackTrace();
		} return "success";
	}
}

审计点:

  1. 查看文件写入路径是否可以被用户控制
  2. 查看是否限制文件后缀

靶场还有文件类型绕过漏洞

image-20260104194622819.png

可以在burp的重放器修改content-type绕过

image-20260104194719396.png

靶场文件下载示例

/**
 * 产生原因:文件路径没做限制,可通过../递归下载任意文件
 * 如下载 filename=../../etc/passwd
 */
public String download(String filename, HttpServletResponse response) {
    String filePath = System.getProperty("user.dir") + "/logs/" + filename;
    try (InputStream inputStream = new BufferedInputStream(Files.newInputStream(Paths.get(filePath)))) {
        response.setHeader("Content-Disposition", "attachment; filename=" + filename);
        response.setContentLength((int) Files.size(Paths.get(filePath)));
        response.setContentType("application/octet-stream");

        IOUtils.copy(inputStream, response.getOutputStream());
        return "下载文件成功:" + filePath;
    } catch (IOException e) {
        return "未找到文件:" + filePath;
    }
}
                    

安全写法是过滤 ../../之类的绕过方法。

解决方案

  1. 检查上传文件扩展名白名单,不属于白名单内,不允许上传。
  2. 上传文件的目录必须是 http 请求无法直接访问到的。如果需要访问的,必须上传到其他(和 web 服务器不同的)域名下,并设置该目录为不解析 jsp 等脚本语言的目录。
  3. 上传文件要保存的文件名和目录名由系统根据时间生成,不允许用户自定义。

重定向漏洞

URL redirect, URL 跳转攻击。

Web 应用程序接收到用户提交的 URL 参数后,没有对参数做“可信任 URL”的验证, 就向用户浏览器返回跳转到该 URL 的指令。当遇到恶意钓鱼网站就会收到攻击。

URL跳转相关函数:

  • sendRedirect()
  • redirect()
  • forward()
  • setHeader()

代码示例

示例1:

if(checklogin(request)){
	response.sendRedirect(request.getParameter("url"));
}

这段代码存在 URL 跳转漏洞,当用户登陆成功后,会跳转到 url 参数所指向的地址。

示例2:

使用SpringMVC时使用 redirect 开头的字符串,可以起到重定向作用

public String redirect(){
	return "redirect:http://www.baidu.com";
}

解决方案

  • 为了保证用户所点击的 URL,是从 web 应用程序中生成的 URL,所以要做 TOKEN 验证。

  • 如果应用只有跳转网站的需求,可以设置白名单,判断目的地址是 否在白名单列表中,如果不在列表中,就判定为 URL 跳转攻击,并记录日志。不允许配置集团以外网站到白名单列表中。

  • 这两个方案都可以保证所有在应用中发出的重定向地址,都是可信任的地址。

注意:设置白名单时要注意进行后缀匹配

CSRF 漏洞

利用有效 cookie 攻击服务器或者执行恶意操作

代码示例

 public Map<String, Object> transferMoney(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
     String from = (String) session.getAttribute("LoginUser");
     String amount = request.getParameter("amount");
     String receiver = request.getParameter("receiver");
     // 转账操作
     ...

     Map<String, Object> result = new HashMap< >();
     result.put("from", from);
     result.put("receiver", receiver);
     result.put("amount", amount);
     result.put("success", true);
     return result;
 }

安全代码

 String token = request.getParameter("csrfToken");
 String sessionToken = (String) session.getAttribute("csrfToken");

 // 校验CSRF Token
 if (!token.equals(sessionToken)) {
     result.put("success", false);
     result.put("message", "token is not valid");
     return result;
 }
                    

信息泄露漏洞

Actuator

Spring Boot Actuator 模块提供了健康检查,审计,指标收集, HTTP 跟踪等,是帮助我们监控和管理Spring Boot 应用的模块。这个模块采集应用的内部信息,展现给外部模块,可以查看应用配置的详细信息,例如自动化配置信息、创建的Spring beans信息、系统环境变量的配置信息以及Web请求的详细信息等。

Actuator 使用

如果要使用 SpringBoot Actuator 提供的监控功能,需要先加入相关的 maven dependency:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-actuator</artifactId>
	<version>2.7.0</version>
</dependency>

只要加上了这个actuator依赖, SpringBoot 在运行时会自动开启/actuator/health和/actuator/info这两个 endpoint。

image-20260104203057153.png 开启了所有端点未授权访问的配置如下:

management.endpoints.shutdown.enabled=true
management.endpoints.web.exposure.include=* 

image-20260104203157822.png

image-20260104203258431.png

默认开放路径是:http://localhost:8888/actuator/

下面是内存信息泄露的文件:

image-20260104203559154.png

image-20260104203654330.png

heapdump 分析工具

下载网址:whwlsfb/JDumpSpider: HeapDump敏感信息提取工具

Endpoints

Spring Boot 提供了所谓的 endpoints(端点)给外部来与应用程序进行访问和交互。

这些 Actuator 模块本来就有的端点我们称之为原生端点。根据端点的作用的话,我们大概可以分为三大类:

  • 应用配置类:获取应用程序中加载的应用配置、环境变量、自动化配置报告等与 Spring Boot应用密切相关的配置类信息。

  • 度量指标类:获取应用程序运行过程中用于监控的度量指标,比如:内存信息、线程池信息、 HTTP请求统计等。

  • 操作控制类:提供了对应用的关闭等操作类功能。

请求方法端点描述
GET/actuator查看有哪些 Actuator端点是开放的。
GET/actuator/auditeventauditevents端点提供有关应用程序审计事件的信息。
GET/actuator/beansbeans端点提供有关应用程序 bean 的信息。
GET/actuator/conditionsconditions端点提供有关配置和自动配置类条件评估的信息。
GET/actuator/configpropsconfigprops端点提供有关应用程序@ConfigurationPropertiesbean的信息。
GET/actuator/env查看全部环境属性,可以看到 SpringBoot 载入哪些properties,以及 properties 的值(会自动用*替换key、 password、 secret 等关键字的 properties 的值)。
GET/actuator/flywayflyway端点提供有关 Flyway 执行的数据库迁移的信息。
GET/actuator/health端点提供有关应用程序运行状况的health详细信息。
GET/actuator/heapdumpheapdump端点提供来自应用程序 JVM 的堆转储。 (通过分析查看/env端点被*号替换到数据的具体值。 )
GET/actuator/httptracehttptrace端点提供有关 HTTP 请求-响应交换的信息。(包括用户HTTP请求的Cookie数据,会造成Cookie泄露等)。
GET/actuator/infoinfo端点提供有关应用程序的一般信息。
GET/actuator/integrationgraphintegrationgraph端点公开了一个包含所有 Spring Integration 组件的图。
GET/actuator/liquibaseliquibase端点提供有关 Liquibase 应用的数据库更改集的信息。
GET/actuator/logfilelogfile端点提供对应用程序日志文件内容的访问。
GET/actuator/loggersloggers端点提供对应用程序记录器及其级别配置的访问。
GET/actuator/mappingsmappings端点提供有关应用程序请求映射的信息。
GET/actuator/metricsmetrics端点提供对应用程序指标的访问。
GET/actuator/prometheus端点以prometheusPrometheus 服务器抓取所需的格式提供 Spring Boot 应用程序的指标。
GET/actuator/quartzquartz端点提供有关由 Quartz 调度程序管理的作业和触发器的信息。
GET/actuator/scheduledtasksscheduledtasks端点提供有关应用程序计划任务的信息。
GET/actuator/sessionssessions端点提供有关由 Spring Session 管理的应用程序 HTTP 会话的信息。
GET/actuator/startupstartup端点提供有关应用程序启动顺序的信息。
POST/actuator/shutdownshutdown端点用于关闭应用程序。

漏洞利用方法

github.com/LandGrey/Sp…

漏洞利用工具

github.com/0x727/Sprin…

Swagger

Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。

若存在相关的配置缺陷,攻击者可以未授权翻查Swagger接口文档,得到系统功能API接口的详细参数,再构造参数发包,通过回显获取系统大量的敏感信息。

用dirsearch扫靶场可以扫出来:

 dirsearch -u http://localhost:8888/  --cookie=你的cookie值

image-20260104211436480.png

image-20260104211542524.png

相关实例和工具:

cloud.tencent.com/developer/a…

github.com/lijiejie/sw…

github.com/godzeo/swag…

github.com/jayus0821/s…

swagger-exp使用

下载后需要修改一下路径函数来指定路径,因为harker机注册表默认没有chrome。

找到commom.py文件

image-20260105104458652.png

修改get_chrome_path_win()函数

# lib/common.py 里的 get_chrome_path_win 函数
def get_chrome_path_win():
    # 直接写你电脑中Chrome的实际路径
    chrome_path = r"chrome安装路径"
    if os.path.exists(chrome_path):
        return chrome_path
    else:
        print("提示:Chrome路径不存在,请手动修改lib/common.py里的路径")
        return None

image-20260105104629002.png

社区有使用说明:

image-20260105104704667.png

扫完就会发现靶场的一些接口存在信息泄露

image-20260105105714763.png

Druid

Druid是阿里巴巴数据库出品的为监控而生的数据库连接池。并且Druid提供的监控功能包括监控SQL的执行时间、监控Web URI的请求、 Session监控等。

Druid本身是不存在什么漏洞的,但当开发者配置不当时就可能造成未授权访问。泄露的session信息会导致权限绕过。

漏洞利用

blog.csdn.net/qq_50854662…

xz.aliyun.com/t/10110

参考

blog.csdn.net/JHIII/artic…

blog.csdn.net/weixin_

JWT弱口令

JSON Web Token 为开发人员提供了几种对有效负载声明进行数字签名的方法。这确保了数据完整性和强大的用户身份验证,每当开发人员使用 HMAC 签名时,他们都需要提供一个密钥,用于签名和验证令牌。如果这个密钥不够强大,整个签名可能会被泄露,造成越权操作等危害。

靶场有案例

/**
 * 产生原因:设置了简单的密钥,可爆破从而进行伪造jwt
 */
private static final String SECRET = "123456";
private static final String B64_SECRET = Base64.getEncoder().encodeToString(SECRET.getBytes(StandardCharsets.UTF_8));

public static String generateTokenByJjwt(String userId) {
    return Jwts.builder()
        .setHeaderParam("typ", "JWT")
        .setHeaderParam("alg", "HS256")
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
        .claim("userid", userId)
        .signWith(SignatureAlgorithm.HS256, B64_SECRET)
        .compact();
}

逻辑漏洞

越权漏洞

靶场有,且php代码审计中讲过,不在重述。

验证码复用

/**
 * 产生原因:未在验证后清除 session 中的验证码,可能导致验证码可复用的问题
 */
if (!CaptchaUtil.ver(captcha, request)) {
    model.addAttribute("msg", "验证码不正确");
    return "login";
}

密码重置

还有种情况是验证码直接返回到了响应包,造成验证码泄露

public Map<String, String> resetPassword(@RequestParam("mobile") String mobile, HttpServletRequest request) {
    Map<String, String> response = new HashMap< >();
    String authCode = generateRandomCode();

    // 发送短信代码
    ...

    // 漏洞点:返回验证码到前端
    response.put("success", "true");
    response.put("mobile", mobile);
    response.put("authCode", authCode);
    return response;
}

image-20260105234847773.png

也可以通过暴力破解验证码来强制重置其他用户密码。

image-20260105235545495.png

华夏ERP实战

部署ERP

华夏ERP(英文名: jshERP)是目前人气领先的国产ERP系统,目前再gitee开源, star数量为11.5k

下载网址:gitee.com/jishenghua/…

华夏ERP使用前后端分离的开发架构,因此需要分别部署

前端

前端部署需要nginx进行代理,因此启动nginx

image-20260107163941767.png

前端dist.zip已经上传云盘。安装方式和普通网站搭建一样。搭建完成后,需要修改网站的配置文件,将后端接口配置正确。

image-20260107164221486.png

image-20260107164253456.png

默认的后端接口如下:

image-20260107164729365.png

完成的配置文件(nginx.conf)已经上传云盘,可以结合自己的域名和路径进行修改,主要是将默认的后端接口修改成如下内容:

location /jshERP-boot/ {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://localhost:9999/jshERP-boot/;
}

服务器要求有Redis库,直接在小皮里安装,安装后启动。

image-20260107164507719.png

image-20260107164524532.png

后端

  1. idea导入源码

选择JSH_ERP-v3.6目录,使用idea导入,并设置jdk为1.8版本

image-20260107170106240.png

image-20260107170128479.png

  1. 导入数据库

找到数据库文件JSH_ERP-v3.6\jshERP-boot\docs\jsh_erp.sql

使用navicate创建jsh_erp数据库并导入对应的sql文件即可。

image-20260107171049914.png

华夏erp默认的连接密码是123456。注意修改application.properties中的mysql数据库密码为root

image-20260107171222604.png

改一下这个路径,跟后面文件上传漏洞有关

image-20260107171354868.png

  1. 初始化redis数据库

华夏erp需要redis数据库,默认情况phpstduy没有安装,在软件管理中安装redis即可。安装完成后,默认的redis密码是空,可以设置成1234abcd,和华夏erp配置文件中的保持一致。 (记得重启生效)

image-20260107192654460.png

搭建成功

访问网站并输入默认账号密码 jsh 123456,即可登录

image-20260107192624143.png

漏洞挖掘

审计时着重关注config,controller(接口),filer(过滤器),uitls(工具类)文件夹

信息泄露1

若服务器上线后没有屏蔽这个接口,会导致信息泄露:

image-20260109154446457.png

image-20260109154542644.png