跳转到帖子

背景

最近协助排查了一起内网业务系统的异常访问事件。系统本身不是互联网暴露资产,但通过办公网、VPN 和若干跳板环境被多人使用。事件最初表现为应用日志中出现大量异常参数、404 探测请求以及少量接口报错。虽然没有确认发生数据泄露,但从日志形态看,已经不再是普通误操作。

这类事件在企业内网里比较常见:系统上线多年,缺少持续安全测试;业务接口依赖历史框架;权限模型逐步叠加,代码里留下不少临时逻辑。排查时如果只盯单个告警,很容易漏掉真正的问题链路。本文整理一套相对通用的分析思路,重点放在日志研判、漏洞定位、代码审计和修复验证,不涉及未授权攻击操作。

问题场景

业务系统为 Java Web 应用,前端通过传统表单和少量 AJAX 接口提交数据,后端使用 Spring MVC + MyBatis。排查入口来自网关日志中的异常请求,特征大致如下:

  • 同一来源 IP 在短时间内访问多个不存在路径,如备份文件、管理入口、测试接口。
  • 部分参数中出现特殊字符组合,例如引号、括号、路径跳转符号。
  • 少量接口返回 500,且集中在查询类接口。
  • 请求 User-Agent 比较混乱,既有正常浏览器,也有脚本化特征。

初步判断,这不是单纯扫描器扫目录,而是有人在根据返回结果逐步调整请求。后续重点放在三个方向:SQL 注入风险、越权访问风险、文件读取/下载风险。

日志分析思路

排查第一步不是马上复现漏洞,而是把请求链路串起来。建议至少保留以下几类日志:网关访问日志、应用访问日志、应用错误日志、数据库慢查询日志、审计日志。如果只有应用日志,定位会困难很多。

可以先按时间窗口聚合异常请求,例如提取状态码、接口路径、来源地址、参数长度等字段:

awk '$9 ~ /404|500|403/ {print $1,$4,$7,$9,$12}' access.log | sort | uniq -c | sort -nr | head

如果日志是 JSON 格式,可以用 jq 进行筛选:

cat access.json | jq -r 'select(.status==500 or .status==403) | [.time,.src_ip,.method,.uri,.status,.ua] | @tsv'

实际排查中比较有价值的不是单条异常请求,而是请求顺序。例如先访问登录页,再访问某个查询接口,随后调整参数并触发 500,这往往说明对方已经发现了可交互接口。

技术分析一:查询接口的 SQL 注入风险

异常最多的是一个历史查询接口,业务上用于按姓名、编号、时间范围筛选记录。代码审计时发现 Mapper 中存在字符串拼接,简化后的问题代码如下:

<select id="listRecord" resultType="Record">
  SELECT id, name, dept, create_time
  FROM t_record
  WHERE 1=1
  <if test="name != null and name != ''">
    AND name LIKE '%${name}%'
  </if>
  <if test="dept != null and dept != ''">
    AND dept = '${dept}'
  </if>
</select>

这里的关键问题是使用了 ${} 进行文本替换,参数会直接进入 SQL 语句。即使接口前端限制了输入字符,也不能作为安全边界。因为攻击者可以直接构造 HTTP 请求,不经过前端校验。

修复方式应改为参数绑定,并对 LIKE 场景做安全拼接:

<select id="listRecord" resultType="Record">
  SELECT id, name, dept, create_time
  FROM t_record
  WHERE 1=1
  <if test="name != null and name != ''">
    AND name LIKE CONCAT('%', #{name}, '%')
  </if>
  <if test="dept != null and dept != ''">
    AND dept = #{dept}
  </if>
</select>

需要注意的是,排序字段、表名、列名这类位置不能直接使用参数绑定。如果业务确实需要动态排序,应使用白名单映射,而不是直接接收前端传来的字段名。

private static final Map<String, String> ORDER_FIELD = Map.of(
    "time", "create_time",
    "name", "name"
);

String orderBy = ORDER_FIELD.getOrDefault(req.getOrderBy(), "create_time");

技术分析二:接口越权与权限校验缺失

第二类问题出现在详情接口。日志中可以看到同一用户连续访问不同记录 ID,且部分记录不属于该用户所在部门。代码层面检查后发现,接口只校验了登录态,没有校验数据归属。

@GetMapping("/record/detail")
public Record detail(Long id) {
    return recordService.getById(id);
}

这类问题通常不依赖复杂技术,但影响很实际。尤其是内网系统,很多开发会默认“能登录就是可信用户”,导致水平越权长期存在。

修复建议是把权限判断放到服务层,避免每个 Controller 自行实现。示例:

public Record getDetail(Long id, CurrentUser user) {
    Record record = recordMapper.selectById(id);
    if (record == null) {
        throw new BizException("record not found");
    }
    if (!permissionService.canReadRecord(user, record)) {
        throw new AccessDeniedException("no permission");
    }
    return record;
}

权限模型尽量不要只依赖前端菜单。菜单控制解决的是“看不看得到入口”,接口鉴权解决的是“能不能访问资源”。两者不是一回事。

技术分析三:文件下载接口的路径处理

排查中还发现少量请求带有 ../ 形式的路径片段。对应接口用于下载业务附件,早期实现直接接收文件名后拼接本地目录:

@GetMapping("/file/download")
public void download(String fileName, HttpServletResponse response) {
    File file = new File(baseDir + "/" + fileName);
    fileService.write(file, response);
}

这类实现容易出现路径穿越风险。安全做法是使用文件 ID 访问,由后端从数据库查真实存储路径;如果历史原因必须接收文件名,也需要做规范化校验,确保最终路径仍位于允许目录下。

Path basePath = Paths.get(baseDir).toAbsolutePath().normalize();
Path targetPath = basePath.resolve(fileName).normalize();

if (!targetPath.startsWith(basePath)) {
    throw new AccessDeniedException("invalid file path");
}

同时应限制可下载文件类型、校验业务权限、避免将服务器绝对路径直接返回给前端。

关键排查步骤

  • 确定时间线:从第一条异常请求开始,按时间排序观察请求变化,避免孤立分析。
  • 关联身份信息:将来源 IP、登录用户、Session、接口路径关联起来,看是否存在正常账号被滥用。
  • 定位高风险接口:优先检查查询、导出、详情、下载、上传、回调类接口。
  • 回看代码实现:重点搜索 ${}、字符串拼接 SQL、文件路径拼接、直接按 ID 查询等模式。
  • 验证修复效果:使用测试环境构造边界输入,确认异常参数被拒绝或安全处理。

建议的代码审计检查点

检查项常见问题修复建议
SQL 查询使用字符串拼接或 MyBatis ${}使用参数绑定,动态字段走白名单
详情接口只校验登录,不校验资源归属服务层统一做对象级权限判断
文件下载前端传路径或文件名直接拼接使用文件 ID,规范化路径并限制目录
导出接口权限范围与页面查询不一致复用查询权限条件,限制导出规模
异常处理返回堆栈、SQL 片段、绝对路径统一异常响应,敏感信息只写内部日志

日志与监控建议

漏洞修完不代表事件结束。对内网系统来说,后续监控同样重要。建议至少补齐以下能力:

  • 记录关键接口的用户 ID、资源 ID、操作结果,不要只记录 URL。
  • 对连续访问大量不同 ID 的行为做告警,尤其是详情和下载接口。
  • 对 403、500、参数校验失败进行聚合统计,观察是否持续出现。
  • 数据库层面开启慢查询和异常查询监控,辅助发现异常访问模式。
  • 保留足够长的网关日志,避免事后只能看到应用片段。

一个简单的异常访问统计思路如下:

# 统计同一用户在单位时间内访问不同资源 ID 的数量
cat app.log | grep "/record/detail" \
  | awk '{print $1,$2,$5,$8}' \
  | sort | uniq -c | sort -nr | head

实际环境中更建议通过日志平台完成,例如按用户、接口、资源 ID 去重统计,再设置合理阈值。阈值不要拍脑袋,先观察一两周正常业务峰值。

修复注意点

  • 不要只修触发告警的接口。同类代码通常会复制到多个模块,应通过关键字和调用链全量排查。
  • 不要依赖前端校验。前端限制只能改善用户体验,不能作为安全边界。
  • 不要把所有异常都吞掉。对外响应可以统一,但内部日志要保留足够定位信息。
  • 权限判断要贴近数据。如果只在菜单、路由层做判断,容易漏掉直接访问接口的场景。
  • 修复后要做回归测试。尤其是历史系统,安全修复可能影响老业务流程。

总结

这次事件没有看到复杂的利用链,更多是常见 Web 风险在内网系统中的集中暴露:SQL 拼接、对象级权限缺失、文件路径处理不严。问题本身并不新,但一旦业务系统长期缺少审计和监控,就会变成真实风险。

比较有效的做法是把排查过程固化下来:从日志建立时间线,从异常接口回到代码,从单点修复扩展到同类模式排查,再通过监控验证是否还有残留行为。对企业内部系统来说,这套方法往往比单纯跑一次扫描器更有价值。

安全排查不要只追求“找到一个洞”,更重要的是找到导致这类漏洞反复出现的开发模式和流程缺口。
代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

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