跳转到帖子

背景

内网 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 白名单工具。这样才能避免同类问题在不同模块反复出现。

审计工作要克制,复现要有边界,报告要能指导修复。这样无论是红队演练、内部评估,还是日常安全建设,产出的价值都会更稳定。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…