首页
社区
课程
招聘
[原创]基于图书管理系统的漏洞复现与代码审计
发表于: 1天前 306

[原创]基于图书管理系统的漏洞复现与代码审计

1天前
306

⚠️ 免责声明:本项目仅用于安全学习与教育目的,严禁用于非法用途。如因使用本项目造成的任何法律后果,使用者自行承担全部责任。


一、项目背景

本项目是基于 SpringBoot + Vue 的图书管理系统,实现了图书查询、借阅、借阅记录查看、用户登录注册等功能。在学习 Web 安全知识后,我对项目进行了主动的代码审计,发现两处高危漏洞:SQL 注入和水平越权。

项目源码:GitHub - library-security-lab


二、SQL 注入漏洞

2.1 漏洞原理

${} 和 #{} 的区别:

@Select("SELECT * FROM books WHERE book_name LIKE '%${bookName}%' AND author LIKE '%${author}%'")

这个是危险的写法${} 是字符串替换,直接把参数拼接到 SQL 里。

举例:

  • 用户输入:Java

  • SQL:SELECT * FROM books WHERE book_name LIKE '%Java%'

  • 用户输入:' OR '1'='1' -- -

  • SQL:SELECT * FROM books WHERE book_name LIKE '%' OR '1'='1' -- -%'

'1'='1' 永远都是真的,逻辑就被篡改了。

@Select("SELECT * FROM books WHERE book_name LIKE #{bookName} AND author LIKE #{author}")

这个是安全的写法#{} 是预编译参数,这些参数会被当做纯文本来处理。

举例:

  • 用户输入:' OR '1'='1' -- -
  • SQL:SELECT * FROM books WHERE book_name LIKE ?(? 是占位符)

数据库收到以后就是普通的字符串,不是 SQL 代码。

2.2 漏洞代码位置

BookMapper.java

@Select("SELECT * FROM books WHERE book_name LIKE '%${bookName}%' AND author LIKE '%${author}%'")

BookService.java

return bookMapper.searchBoooksVuln(
    nameEmpty ? "%" : bookName,
    authorEmpty ? "%" : author
);

问题: 直接把前端的数据喂给后端,没有做任何的处理。

2.3 复现步骤

步骤 1:验证漏洞存在

Payload:

bookName=' OR '1'='1' -- -

解释:

  1. ':闭合前面的单引号
  2. '1'='1':添加了一个永远都是真的条件
  3. -- -:注释掉后面的 AND author LIKE '%...%'-- 是 SQL 注释符,后面必须跟一个空格)

在 Burp 里操作就是:

bookName=%27%20OR%20%271%27%3D%271%27%20--%20-

%27'%20 → 空格

SQL 注入验证

SQL 注入验证结果

可以看到图片里返回了 33 本书,这是正确的。

步骤 2:判断列数(ORDER BY)

理由: 后面的 UNION SELECT 要让左右列数一样,不然会报错。

Payload:

ORDER BY 5 -- -

这里我是知道我自己的页数,在不知道的情况下需要一个个试。

  • 如果正常返回:列数 >= 5
  • 报错或者空:列数 < 5

5√ 6 × → 列数就是 5

ORDER BY 判断列数 1

ORDER BY 判断列数 2

这一步我们就确定了列数是 5。

步骤 3:找显示位

UNION 可以把两个查询结果拼接到一起,构造一个"假查询",数据就会显示在页面上。

Payload:

bookName=' UNION SELECT 1,2,3,4,5 -- -

右边的就是我们构造的假查询。

找显示位

我这个有分页,而且没有排序,所以他会显示在最后一页,可以看到这个图片里显示了数字,这些就是我们找到的显示位。

步骤 4:爆数据库版本

主要是确认一下环境,找到这个 MySQL 的版本号,这里的显示位我用的是第 2 列。

Payload:

bookName=' UNION SELECT 0,version(),3,4,5 -- -
  • 0:id=0
  • version():数据库内置函数,返回数据库版本

爆数据库版本 1

爆数据库版本 2

8.0.45 就是我们提取出来的版本号。

步骤 5:爆表名(获得数据库结构)

主要查看数据库里有哪些表格。

Payload:

bookName=' UNION SELECT 1,table_name,3,4,5 FROM information_schema.tables WHERE table_schema=database() -- -

information_schema.tables 是 MySQL 的系统表,存储了所有表的信息。

table_schema=database() 表示查询当前数据库的表。

还是放在第 2 列显示。

爆表名 1

爆表名 2

出现这个问题不要慌,查一下是不是格式有问题,根据上面的步骤统一一下编码,或者刷新一下重新抓包。

爆表名问题

爆表名成功

这四个表格就是我的数据库里的。

步骤 6:爆字段名(找账号和密码)

user 表里有哪些列(id, username, password 等)。

还是一样的查 information_schema,但这里是 columns

Payload:

bookName=' UNION SELECT 0,column_name,3,4,5 FROM information_schema.columns WHERE table_name='user' -- -

爆字段名 1

爆字段名 2

爆字段名 3

确认有 usernamepassword 就可以。

步骤 7:拖库(偷 username 和 password)

CONCAT 函数把这两个字段连接起来。

Payload:

bookName=' UNION SELECT 0,CONCAT(username,':',password),4,5 FROM user -- -

注: 一定要注意符号,容易漏打。

拖库 1

拖库成功

我这里有两个用户,可以看到完整的显示了。

这个就是全过程。

2.4 修复方案

修复代码:

// 【修复代码】使用 #{} 预编译参数,防止 SQL 注入
@Select("SELECT * FROM books WHERE book_name LIKE #{bookName} AND author LIKE #{author}")
List<Book> searchBooksSafe(@Param("bookName") String bookName,
    @Param("author") String author);
// 在 Java 层拼接通配符,SQL 使用 #{} 预编译
return bookMapper.searchBooksSafe(
    nameEmpty ? "%" : "%" + bookName + "%", 
    authorEmpty ? "%" : "%" + author + "%"
);

换成 #{} 后,不能再在 SQL 里写 % 了,因为 #{} 会把整个参数(包括 %)当作一个整体去匹配,所以要放到 Java 里去拼接。

  • 如果用户没输入书名(nameEmptytrue),就传 "%"(匹配所有)
  • 如果用户输入了 Java,就拼接成 "%Java%",然后传给 Mapper

这样既能模糊搜索,又不会有 SQL 注入风险。

注意: 若需严格避免用户输入 %_ 被当作 LIKE 通配符,可对用户输入进行转义(如将 % 替换为 \%),或改用全文索引方案。本系统为演示场景,暂不处理。


三、水平越权漏洞

3.1 漏洞描述

通俗理解就是,A 和 B 等级权限相同,但是 A 可以获取 B 的信息,就像取快递,A 报 B 的取件码,把 B 的快递取走。

本系统中,用户 A 可以通过修改 URL 中的 userId 参数,直接查看用户 B 的借阅记录。

3.2 漏洞代码位置

BorrowRecordController.java

try {
    // 直接用的前端传来的,没有判断用户,直接改 URL 里的 userId=2 就能看别人记录
    List<BorrowRecord> records = borrowRecordService.getBorrowRecordsByUserId(userId);
    return Result.success(records);
}

问题: 这里的逻辑就是,前端想查就查,后端太信任,要什么给什么,没有查询是否是当前登录的用户。

3.3 复现步骤

步骤 1:准备两个账号

在网页里注册两个账号,这里我已经注册好了。

前面的截图里可以看到。

步骤 2:登录其中一个账号,正常抓包

水平越权抓包

直接把这里的 userId=1 改成 2

修改 userId



越权成功

旁边就是 user2 的借阅记录,user 就可以查看任何人的记录。

3.4 修复方案

修复代码:

@GetMapping("/records")
public Result getBorrowRecords(HttpServletRequest request) {
    // 修复:从 Session 中获取当前登录用户的真实 ID
    HttpSession session = request.getSession();
    User currentUser = (User) session.getAttribute("loginUser");

    if (currentUser == null) {
        return Result.error("请先登录");
    }

    // 使用 Session 里的真实 ID,完全忽略前端传来的参数
    List<BorrowRecord> records = borrowRecordService.getBorrowRecordsByUserId(currentUser.getId());
    return Result.success(records);
}
// 修复:登录成功后,将用户信息存入 Session
HttpSession session = request.getSession();
session.setAttribute("loginUser", loginUser);
// 从服务器内存里把刚才登录时存进去的 User 对象取出来。

loginUser.setPassword(null); // 脱敏处理
return Result.success(loginUser);

修复原理:

水平越权产生的根本原因是后端过度信任前端参数

修复的核心思想是身份强校验

我在登录接口中引入了 HttpSession,用户登录成功后将 User 对象存入服务器 Session 中。在查询借阅记录的接口中,不再接收前端传来的 userId,而是直接从 Session 中解析出当前登录用户的真实 ID 进行查询。

这样,即使用户通过抓包工具篡改了请求参数,后端依然只返回属于他本人的数据,彻底阻断了越权路径。

  • Session 是服务器端的一块内存空间。每个用户登录后,服务器都会给他分配一个唯一的 Session ID,存在浏览器的 Cookie 里,只要带着这个请求,服务器就知道是刚才登录的人。

四、总结与反思

通过本次对图书管理系统的代码审计,我获得了以下收获:

  1. 安全编码意识:永远不要信任前端传来的数据。SQL 注入和越权的根源都是后端过度信任用户输入。
  2. 防御的黄金法则:防 SQL 注入用预编译(#{}),防越权用服务端 Session 校验,这两条是 Web 安全的基本功。
  3. 攻击视角的价值:亲自走一遍 UNION 注入的完整流程,比只看理论更能理解漏洞的危害性。
  4. 文档沉淀的重要性:将审计过程写成博客,既是自我复盘,也是向他人展示能力的有效方式。

GitHub 项目地址:42aK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1j5I4y4r3k6J5k6h3g2Q4x3V1k6D9K9h3u0J5j5i4u0&6i4K6u0V1M7$3g2U0N6i4u0A6N6s2W2Q4x3X3c8D9j5h3t1`.

欢迎 Star 和交流!



传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (1)
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2

这篇文章把 SpringBoot + Vue 项目中常见的两类高危漏洞讲得很透彻!尤其是 SQL 注入部分,直接点出 ${} 和 #{} 的本质区别——很多人刚接触 MyBatis 时容易忽略这个坑。如果你对网络安全知识感兴趣,推荐访问:463K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2B7j5%4N6D9P5h3k6Q4x3X3g2U0L8$3@1`.

最后于 8小时前 被mb_treykprh编辑 ,原因:
8小时前
0
游客
登录 | 注册 方可回帖
返回