java 文件有固定结构,按固定顺序读,就不会漏东西。
| 符号 |
意思 |
{} 花括号 |
圈定范围。类的内容、方法的内容都装在花括号里 |
; 分号 |
一句话说完。每个”动作”结尾基本都是它 |
() 圆括号 |
装参数。方法名后面跟的括号里写输入 |
<> 尖括号 |
泛型,说明”这里面装的是什么类型的东西” |
@ 注解 |
贴给框架看的标签,不影响程序逻辑,但决定框架怎么对待这段代码 |
此处代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| package com.best.hello.controller.IDOR;
import com.alibaba.fastjson.JSON; import com.best.hello.entity.User; import com.best.hello.entity.UserIDOR; import com.best.hello.mapper.UserMapper; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession; import java.util.HashMap; import java.util.List; import java.util.Map;
@Api("水平越权") @Slf4j @RestController @RequestMapping("/vulnapi/IDOR")
public class IDOR1 {
@Autowired private UserMapper userMapper;
@ApiOperation(value = "vul:根据name查询用户信息") @GetMapping("/vul/info") public List<UserIDOR> vul(String name) { log.info("[vul] 基于用户身份越权查询:{}", name); return userMapper.queryByUser2(name); }
@ApiOperation(value = "vul:基于对象ID越权查询") @GetMapping(value = "/vul/userid") public List<User> vul2(Integer id) { log.info("[vul] 基于对象ID越权查询:{}", id); return userMapper.queryByIdAsInterger(id); }
@ApiOperation(value = "safe:只允许查询自己信息") @GetMapping("/safe/info") public Object safe(String name, HttpSession session) { log.info("[safe] 水平越权查询:{}", name); String loginUser = (String) session.getAttribute("LoginUser"); Map<String, String> m = new HashMap<>();
if (loginUser.equals(name)) { m.put("user", loginUser); } else { m.put("info", "无权限查询其他用户!"); } return JSON.toJSONString(m); }
@ApiOperation(value = "safe:通过UUID查询用户信息") @GetMapping("/safe/userid") public List<User> safe2(@RequestParam("uuid") String uuid) { log.info("[safe] 查询UUID {}", uuid); return userMapper.queryByUuid(uuid); }
}
|
步骤1:看 package —— 在哪个目录
1
| package com.best.hello.controller.IDOR;
|
人话:对应的就是文件夹层级 com/best/hello/controller/IDOR/
审计:包名必须和文件夹路径一致(Java 的硬性要求),而且包名说明文件的角色 —— controller是接收请求的入口,mapper是查数据库的,util是工具类。拿到陌生项目,先看包名就能大致画出结构。
步骤 2:扫 import —— 用了什么组件
人话:把别的类借到本文件里来用。这文件要用的外部东西,都得先 import 进来。
审计: 逐个扫 import,看到 fastjson、xstream、log4j、shiro 这类名字就标记下来 —— 这是依赖漏洞,可能不用读懂业务逻辑就能出洞。另外看到 javax.servlet 说明是 Web 项目,看到 java.sql 说明碰数据库。
步骤 3:看类声明和类上的注解 —— 这是什么类
人话:public class IDOR1 { 是类声明 —— 类就是一组代码的容器,名字叫 IDOR1,而且类名必须和文件名一致。上面那三行 @ 开头的注解是贴给框架的标签:@RestController 意思是”这个类里方法的返回值直接当 HTTP 响应发出去”,@RequestMapping("/vulnapi/IDOR") 给整个类定一个 URL 前缀。
审计:@RestController + @RequestMapping 凑在一起 = 一个 Web 入口类
真实 URL = 类上的 @RequestMapping + 方法上的路径
步骤 4:看字段
1 2
| @Autowired private UserMapper userMapper;
|
人话:这两行在类里面、但不在任何方法里,叫字段(也可以叫成员变量)。它像这个类随身带着的工具。private 是访问修饰符,表示”只有本类能用”;@Autowired 是说”这个对象我不用自己 new,框架会帮我塞进来”。
审计信号:在 IDEA 里把光标放在 userMapper 上按 Ctrl+B(或 Ctrl+鼠标左键),就能跳到它的定义或实现。
步骤 5:看方法 —— 一个方法就是一条数据流
1 2 3 4 5
| @GetMapping("/vul/info") public List<UserIDOR> vul(String name) { log.info("[vul] 基于用户身份越权查询:{}", name); return userMapper.queryByUser2(name); }
|
(1) 方法上的注解 → 路径和请求方式
@GetMapping(“/vul/info”) 表示这是 GET 请求,路径是 /vulnapi/IDOR/vul/info。@GetMapping /@PostMapping 都是接口入口的标志,数一数有几个,就是这个文件有几个接口。
(2) 方法签名 → 输入和输出
public List vul(String name) 拆开看。此处略。
审计信号:参数 name 上没加任何注解 → Spring 会自动把 URL 里的查询参数绑给它。这就是 Source(用户输入进来的地方)。
(3) 方法体 → 数据流到哪去了
1
| return userMapper.queryByUser2(name);
|
name被原封不动交给查询,没有验证这个用户的身份。Source 直接流向 Sink,中间没有任何校验 —— 这就是水平越权。
(4) return → 数据的出口
return 把结果还给调用者(这里是还给浏览器)。返回值也是要注意的:如果它把用户输入原样回显到页面上,那就是 XSS 的温床。
此处是靶场练习,可用多一个:
步骤 6:对照一下修复版
同一个文件里的 safe() 方法(第 46-59 行)是修复版,对比着看
1 2 3 4 5 6
| String loginUser = (String) session.getAttribute("LoginUser"); if (loginUser.equals(name)) { m.put("user", loginUser); } else { m.put("info", "无权限查询其他用户!"); }
|
查询前先拿 session 里的登录身份做了一次比对。vul() 里完全没有这一步,所以谁都能查别人。
任何跟”查看/修改/删除”有关的接口,都要问一句:它有没有先确认你是谁
完整数据流:/vulnapi/IDOR/vul/info?name=admin
resource到sink:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| ① 浏览器 GET /vulnapi/IDOR/vul/info?name=admin ↓ ② Spring 路由匹配 类上 @RequestMapping("/vulnapi/IDOR") ← IDOR1.java:24 方法上 @GetMapping("/vul/info") ← IDOR1.java:32 两段拼起来命中 → IDOR1.vul() ↓ ③ 参数绑定 ← ★ Source 在这里诞生 vul(String name) ← IDOR1.java:33 参数没写注解,Spring 自动去 URL 查询串里取值塞进来 此时 name = "admin" ↓ ④ 拦截器 LoginHandlerInterceptor.preHandle() 只检查 session 里有没有 LoginUser ⚠️ 它只管"你登没登录",不管"你能查谁" ← 这是新手最容易误判的一步 ↓ ⑤ 进入方法体 IDOR1.java:34 log.info(...) ← 只是打日志,不是校验 IDOR1.java:35 userMapper.queryByUser2(name) ↓ ⚠️ 数据流在这里跨文件断掉 → Ctrl+B 跳过去 ↓ ⑥ UserMapper.java:31 接口方法(只有声明,没有实现体) 头顶挂着 UserMapper.java:30 的 @Select("select * from users where user = #{user}") ↓ ⑦ MyBatis 的动态代理拦下这次调用,读出 @Select 里的 SQL SQL = select * from users where user = #{user} ↓ ⑧ #{user} 走预编译(PreparedStatement) 参数值作为"数据"单独传给数据库,不拼进 SQL 文本 → 这一步是干净的,没有 SQL 注入 ↓ ⑨ MySQL 实际执行 SELECT * FROM users WHERE user = 'admin' ↓ ⑩ 结果集 → 映射成 List<UserIDOR> (列名对应关系由 UserMapper.xml 里的 resultMap 管) ↓ ⑪ 回到 IDOR1.vul() 的 return @RestController 让返回值直接序列化成 JSON 响应 ↓ ⑫ 浏览器看到 admin 的信息
|
复盘:
1、只有标了 @GetMapping / @PostMapping 的才是接口。数注解,不数路径。
比如,这个文件只有这四个接口:
1 2 3 4
| @GetMapping("/vul/info") @GetMapping(value = "/vul/userid") @GetMapping("/safe/info") @GetMapping("/safe/userid")
|
2、注释要验证,但不要盲信也不要无视。
注释当线索用,最后以代码和配置为准
3、${} 不等于一定有洞,还要看参数类型
看到 ${} 只是锁定嫌疑人,不等于定罪。比如顺着数据流向查的时候,发现有个地方,因为参数被声明成了 Integer,Spring 会强制做类型转换,乱传字符串会直接报类型错误。
| # |
条件 |
缺了会怎样 |
| 1 |
危险写法 —— ${} / 字符串拼接 |
没有它就没什么可聊的 |
| 2 |
参数能带脏数据 —— 类型是 String 这类 |
Integer 会被强制转换挡掉 |
| 3 |
路径可达 —— 从入口到这里没有被映射/白名单/正则拦住 |
拦住了就是安全的 |
比如:
1 2 3
| @Select("select * from users where id = ${id}") List<User> queryByIdAsString(@Param("id") String id); List<User> queryByIdAsInterger(@Param("id") Integer id);
|
后面再详细说。
4、一条链最终算什么漏洞,由 Controller(入口)决定
真实项目里同一个 Mapper 方法会被调用几十次,所以永远不能只看 SQL 就下结论,必须回到调用点看上下文。
5、本项目中UserMapper.java里的几个真注入接口:
本来没想写这个,但是这里顺着链条查到UserMapper.java里面的几种不同写法,然后这里面有的是预编译的不能注入,有的是类型不能注入,剩下的是能真注入的,顺手就给记下来。
如

其他图片略。
| Mapper 方法 |
SQL |
参数类型 |
被谁调用 |
结论 |
queryByIdAsInterger |
where id = ${id} |
Integer |
IDOR1.java:42、MyBatis.java:75 |
注入不了(类型挡了) |
queryByIdAsString |
where id = ${id} |
String |
MyBatis.java:27 |
真注入 |
searchVul |
like '%${user}%' |
String |
MyBatis.java:34 |
真注入 |
orderBy |
order by ${field} ${sort}(在 XML 里) |
String |
MyBatis.java:46 |
真注入 |
有以下几个真注入接口:
1 2 3
| /vulnapi/sqli/mybatis/vul/id/1 union select 1,2,3,4 /vulnapi/sqli/mybatis/vul/search?user=admin /vulnapi/sqli/mybatis/vul/order?field=id&sort=desc
|
