背景
内网 Web 资产往往不像公网业务那样有完整的安全基线:历史系统多、框架版本杂、账号体系分散,很多接口长期处于“能用就行”的状态。做红队演练或内部安全评估时,如果只依赖自动化扫描,很容易漏掉业务逻辑问题;如果只靠手工黑盒测试,又会受限于接口覆盖率。
这篇文章整理一套我在实际代码审计和漏洞复现中常用的思路,重点放在可验证、可复盘、可修复的流程上。内容以防御和内部授权测试为前提,不涉及未授权攻击、凭证窃取或恶意控制。
问题场景
假设目标是一套常见的 Java Web 内部管理系统,技术栈大致如下:
- Spring Boot / Spring MVC
- MyBatis 或 MyBatis-Plus
- Shiro / Spring Security 做认证鉴权
- 前后端分离,接口以 JSON 为主
- 部分文件上传、导入导出、任务调度、报表查询功能
这类系统的风险通常不在单点框架漏洞,而在“权限边界不清 + 参数流向复杂 + 历史代码缺少统一约束”。审计时我一般先不急着看具体漏洞点,而是先建立三张图:接口图、权限图、数据流图。
第一步:快速建立接口资产视图
代码审计的第一件事是知道系统暴露了什么接口。Spring 项目可以从 Controller 入手,重点关注注解路由、权限注解和危险参数。
grep -R "@RequestMapping\|@GetMapping\|@PostMapping\|@PutMapping\|@DeleteMapping" -n src/main/java如果项目规模较大,可以先导出路由清单,标记以下信息:
| 字段 | 说明 |
|---|---|
| 接口路径 | Controller 映射路径和方法路径拼接后的完整路径 |
| 请求方法 | GET、POST、PUT、DELETE 等 |
| 权限控制 | 是否存在 @PreAuthorize、@RequiresPermissions 等 |
| 敏感操作 | 新增、删除、导出、导入、审批、重置密码等 |
| 关键参数 | id、userId、roleId、deptId、file、path、url、sql 等 |
这里要特别注意两类接口:一类是没有权限注解但能修改数据的接口,另一类是有权限注解但只控制菜单权限、没有控制数据范围的接口。
第二步:审计鉴权和数据权限边界
很多内网系统并不是完全没有鉴权,而是“鉴权做了,数据权限没做好”。常见问题包括越权查看、越权修改、跨部门数据访问、普通用户调用管理员接口等。
审计时建议先看全局拦截器或安全配置,例如 Spring Security 的配置类、Shiro Filter Chain、登录态解析逻辑。需要确认:
- 哪些路径被放行,例如 /api/**、/common/**、/profile/** 是否过宽
- 权限注解是否默认启用,是否存在注解写了但未生效的情况
- 用户身份从哪里取,是否可能被前端参数覆盖
- 数据范围是在 SQL 层控制,还是业务代码中临时拼接
一个常见的危险写法是后端直接信任前端传入的 userId、deptId:
// 不推荐:直接使用前端传入的 userId 查询数据
@GetMapping("/orders")
public List<Order> list(Long userId) {
return orderService.listByUserId(userId);
}更稳妥的方式是从服务端会话或 Token 中取当前用户身份,并在服务层统一加数据范围约束:
@GetMapping("/orders")
public List<Order> list() {
Long currentUserId = SecurityUtils.getCurrentUserId();
return orderService.listByUserId(currentUserId);
}如果是管理员查询,应明确区分“功能权限”和“数据权限”,不要仅凭是否能访问菜单来决定能否查看全部数据。
第三步:跟踪参数进入 SQL 的路径
SQL 注入在现代 ORM 项目里少了很多,但并没有消失。MyBatis 项目尤其要关注 ${}、动态排序、动态表名、报表查询、自定义筛选条件。
grep -R "\${" -n src/main/resources src/main/java重点排查以下写法:
- order by ${sortField}
- limit ${offset}, ${pageSize}
- from ${tableName}
- where ${customCondition}
- like '%${keyword}%'
并不是所有 ${} 都一定有漏洞,关键要看参数来源和白名单约束。例如排序字段可以做枚举映射,而不是直接拼接:
private static final Map<String, String> SORT_FIELD_MAP = Map.of(
"createTime", "create_time",
"name", "name"
);
String column = SORT_FIELD_MAP.getOrDefault(req.getSortField(), "create_time");审计 SQL 风险时,我通常按照“Controller 参数 - DTO - Service - Mapper XML”的链路追踪,确认用户可控参数是否进入拼接点。能用 #{} 的地方不要用 ${},确实需要动态字段时必须白名单化。
第四步:文件上传与导入导出功能
文件相关功能是内部系统高频风险点,尤其是头像上传、附件上传、Excel 导入、报表导出、模板下载。审计时不要只看扩展名校验,还要看存储路径、访问路径、解析逻辑和清理策略。
建议检查以下点:
- 是否限制文件大小,避免磁盘被打满
- 是否校验 MIME、扩展名、文件头,且以后端校验为准
- 上传目录是否位于 Web 可执行目录下
- 文件名是否重命名,避免路径穿越和覆盖已有文件
- 是否允许上传压缩包,解压时是否防 Zip Slip
- Excel/CSV 导入是否存在公式注入风险
路径拼接是一个常见坑。下面这种写法如果 filename 可控,容易产生目录穿越风险:
Path target = Paths.get(uploadDir + "/" + filename);更安全的做法是生成随机文件名,并在最终路径上做归一化校验:
String safeName = UUID.randomUUID() + ext;
Path base = Paths.get(uploadDir).toAbsolutePath().normalize();
Path target = base.resolve(safeName).normalize();
if (!target.startsWith(base)) {
throw new IllegalArgumentException("invalid file path");
}对于导出功能,还要关注是否存在批量导出敏感数据的问题。很多泄露并不是因为接口未授权,而是普通账号能导出超出业务范围的数据。
第五步:危险功能和系统调用
内网管理系统里经常有一些“方便运维”的功能,例如在线日志查看、执行脚本、数据库备份、配置测试、URL 连通性检测等。这些功能本身不一定有问题,但权限和参数控制必须非常严格。
代码中可以优先搜索以下关键词:
grep -R "Runtime.getRuntime\|ProcessBuilder\|exec(\|new URL\|HttpClient\|RestTemplate\|FileInputStream\|FileOutputStream" -n src/main/java需要重点判断:
- 命令参数是否完全由用户输入决定
- 是否存在 URL 请求转发功能,可能引发 SSRF
- 读取文件接口是否允许传入任意路径
- 日志查看是否可读取任意日志文件
- 配置测试接口是否会返回过多错误细节
例如“URL 连通性检测”功能,建议只允许访问预设域名或内置服务列表,不要做任意 URL 请求。即使是内部系统,也应限制协议、地址段、端口和重定向。
第六步:反序列化与表达式执行风险
老项目里常见的风险还包括反序列化、表达式解析、模板渲染和规则引擎。审计时可以从依赖和关键词两方面入手。
grep -R "readObject\|ObjectInputStream\|parseExpression\|SpEL\|eval\|ScriptEngine\|Velocity\|Freemarker" -n src/main/java pom.xml如果业务确实需要解析表达式,应尽量使用受限上下文,禁止访问任意类型、方法和系统属性。模板渲染也不要直接使用用户提交的模板内容,更不要把高权限对象放入模板上下文。
漏洞复现时的边界
内部安全评估中,复现漏洞的目标是证明风险存在,而不是扩大影响。建议遵守几个边界:
- 只在授权环境和授权账号下测试
- 优先使用无害参数和测试数据
- 避免读取真实敏感数据,可用数据条数、字段名、错误回显证明问题
- 不执行破坏性操作,例如删除、篡改、持久化控制
- 每一步保留请求、响应、日志和代码位置,方便开发复盘
一份可落地的漏洞记录至少应包含:影响接口、权限前提、参数位置、代码调用链、风险说明、复现截图或日志、修复建议、回归验证方式。
修复建议的写法
很多审计报告的问题在于只写“存在越权”“存在 SQL 注入风险”,开发很难直接改。我的经验是修复建议要尽量贴近代码。
- 越权问题:说明应从服务端当前身份取值,并在 Service 层增加数据范围过滤
- SQL 拼接:指出具体 Mapper 和字段,给出白名单映射方案
- 文件上传:说明文件名重命名、路径归一化、上传目录隔离、大小限制
- SSRF 风险:说明协议白名单、域名/IP 白名单、禁止重定向或重定向后重新校验
- 敏感信息泄露:说明错误信息分级、日志脱敏、接口响应字段裁剪
好的修复建议不是“加校验”三个字,而是告诉开发在哪一层加、校验什么、失败时怎么处理、如何回归验证。
一次完整审计的检查清单
| 类别 | 重点 |
|---|---|
| 认证鉴权 | 放行路径、权限注解、角色边界、数据范围 |
| 输入处理 | 参数类型、长度、枚举、空值、批量 ID |
| SQL 风险 | ${} 拼接、动态排序、动态表名、报表查询 |
| 文件功能 | 上传、下载、预览、解压、导入导出 |
| 危险调用 | 命令执行、URL 请求、文件读取、脚本引擎 |
| 敏感数据 | 日志、导出、接口返回、错误回显、配置文件 |
| 依赖组件 | 框架版本、历史漏洞、废弃组件、测试接口 |
总结
内网 Web 资产的代码审计,核心不是把所有漏洞类型都扫一遍,而是围绕业务边界建立可验证的审计路径。先梳理接口,再确认权限,再跟踪关键参数流向,最后针对文件、SQL、危险功能等高风险点做深入验证。
实践中最有价值的结论通常不是“发现了某个漏洞”,而是找到一类代码模式的问题,并推动形成统一修复方案。例如统一的数据权限拦截器、统一上传组件、统一导出审批、统一 SQL 白名单工具。这样才能避免同类问题在不同模块反复出现。
审计工作要克制,复现要有边界,报告要能指导修复。这样无论是红队演练、内部评估,还是日常安全建设,产出的价值都会更稳定。
推荐意见