跳到主要内容

代码审计:使用 SyntaxFlow 规则检测逻辑漏洞

· 阅读需 10 分钟
Yak ProjectYak Project

逻辑漏洞往往隐藏在业务代码深处,传统扫描工具难以覆盖。本文以 fuint 和 forest 两个开源项目中被分配了 CVE 编号的真实漏洞为例,分享如何通过 IRify 的 SyntaxFlow 语法编写规则,快速定位 Token 伪造与未授权访问等高危逻辑漏洞。

在代码审计和安全研究中,逻辑漏洞(Logic Vulnerabilities)一直是最难攻坚的堡垒。不同于 SQL 注入或 XSS 这种有明显特征的漏洞,逻辑漏洞通常依赖于具体的业务上下文,例如权限校验缺失、关键数据被篡改等。

最近,我们在对开源项目的审计中,利用静态代码分析工具 IRify 及其查询语言 SyntaxFlow,成功发现了几个严重的安全隐患,并被分配了 CVE 编号。今天就通过这三个 CVE 案例,来聊聊如何用“代码”来找“代码中的漏洞”。

CVE-2025-12623 -- 当手机号变成了 Token

项目信息

**漏洞项目:**fuint (门店会员营销系统)

漏洞编号: CVE-2025-12623

漏洞简介:在 ClientS``ignController.java 的登录逻辑中,我们要寻找的是典型的“逻辑覆盖”错误。原本系统设计了安全的随机 Token 生成机制,但在保存数据的前一刻,开发者犯了一个致命错误。

漏洞现场

// ClientSignController.java - Line 324-349
// 方式2:通过账号密码登录
if (StringUtil.isNotEmpty(account) && StringUtil.isNotEmpty(password) && StringUtil.isNotEmpty(captchaCode)) {
Boolean captchaVerify = captchaService.checkCodeByUuid(captchaCode, uuid);
if (!captchaVerify) {
return getFailureResult(201,"图形验证码有误");
}
MtUser userInfo = memberService.queryMemberByName(merchantId, account);
if (userInfo != null) {
String myPassword = userInfo.getPassword();
String inputPassword = memberService.deCodePassword(password, userInfo.getSalt());
if (myPassword.equals(inputPassword)) {
UserInfo loginInfo = new UserInfo();
// Step 1: Generate secure token (e.g., "e5d8f6a3b2c4d1a7f9e8...")
loginInfo.setToken(TokenUtil.generateToken(userAgent, userInfo.getId()));
loginInfo.setId(userInfo.getId());
// Step 2: ❌ CRITICAL BUG - Overwrite secure token with mobile number
loginInfo.setToken(userInfo.getMobile());
TokenUtil.saveToken(loginInfo); // Save phone number as token in Redis
dto.setToken(loginInfo.getToken()); // Return phone number to client
mtUser = userInfo;
} else {
return getFailureResult(201, "账号或密码有误");
}
}
}

这意味着,攻击者只要知道用户的手机号,就可以直接将其作为 Token 进行 API 调用,完全接管该用户的账户。

如下图:

IRify 挖掘思路

这个漏洞的特征在于setT``oken方法被调用时,传入的参数竟然不是一个生成的 Token(通常包含 "token" 字样的方法调用),而是一个普通的手机号字段。

我们利用 IRify 编写了如下规则来捕获这种异常行为:

./(?i)(saveToken|setToken)/?(,* ?{!have:/(?i)token/} ) as $toCheck

规则解析:

  • ./(?i)(saveToken|setToken)/: 使用正则搜索所有名为 saveTokensetToken 的方法调用。

  • ?{!have:/(?i)token/}: 这是一个过滤语法。它检查传入的参数,如果在参数的文本表示中不包含 "token" 字符串(忽略大小写),则命中规则。

在上述代码中,userInfo.getM``obile() 作为参数,里面完全没有 "token" 这个词,因此被精准识别出来:

CVE-2025-12924 & 12925 -- 消失的权限校验门神

项目信息

**漏洞项目:**rymcu/forest (社区论坛系统)

在 forest 项目中,我们发现了两个典型的未授权访问(越权)漏洞,它们虽然位于不同的控制器中,但根源都是鉴权逻辑的缺失。

漏洞现场

CVE-2025-12924:敏感字典随意改

UserDicController 控制着系统的搜索字典。开发者虽然写了 Controller,但遗漏了权限控制注解。导致任何未登录用户都可以调用接口,随意增删改系统字典,甚至实施存储型 XSS(通过字典内容)或拒绝服务攻击。

漏洞现场:

// UserDicController.java - Lines 30-60
@RestController
@RequestMapping("/api/v1/lucene/dic")
public class UserDicController {
@Resource
private UserDicService dicService;
// ❌ NO AUTHENTICATION - Anyone can view all dictionaries
@GetMapping("/getAll")
public GlobalResult getAll(
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "10") Integer rows) {
PageHelper.startPage(page, rows);
List<UserDic> list = dicService.getAll();
PageInfo<UserDic> pageInfo = new PageInfo<>(list);
Map<String, Object> map = new HashMap<>(2);
map.put("userDic", pageInfo.getList());
Map pagination = Utils.getPagination(pageInfo);
map.put("pagination", pagination);
return GlobalResultGenerator.genSuccessResult(map);
}
// ❌ CRITICAL - Anyone can add malicious words to system dictionary
@PostMapping("/addDic/{dic}")
public GlobalResult addDic(@PathVariableString dic) {
dicService.addDic(dic); // Writes to database AND file system
return GlobalResultGenerator.genSuccessResult("新增字典成功");
}
// ❌ CRITICAL - Anyone can modify existing dictionary entries
@PutMapping("/editDic")
public GlobalResult getAllDic(@RequestBodyUserDic dic) {
dicService.updateDic(dic); // Updates database AND regenerates file
return GlobalResultGenerator.genSuccessResult("更新字典成功");
}
// ❌ CRITICAL - Anyone can delete critical dictionary entries
@DeleteMapping("/deleteDic/{id}")
public GlobalResult deleteDic(@PathVariableString id) {
dicService.deleteDic(id); // Deletes from database AND regenerates file
return GlobalResultGenerator.genSuccessResult("删除字典成功");
}
}

CVE-2025-12925:银行资金流水裸奔

BankController 位于 /api/v1/admin/bank 路径下,看名字是管理员专用的接口。然而,代码中虽然有 JWT 认证,却缺少了角色校验(如 @RequiresRoles("admin"))。任何注册的普通用户,只要带着自己的 Token 访问该接口,就能查看平台所有用户的银行账户余额和流水信息。

漏洞现场:

// BankController.java - Lines 20-35
@RestController
@RequestMapping("/api/v1/admin/bank") // ⚠️ Path contains "admin" but has NO admin check!
public class BankController {
@Resource
private BankService bankService;
// ❌ CRITICAL - No authentication, no authorization, complete exposure
@GetMapping("/list")
public GlobalResult<PageInfo<BankDTO>> banks(
@RequestParam(defaultValue = "0") Integer page,
@RequestParam(defaultValue = "10") Integer rows) {
PageHelper.startPage(page, rows);
List<BankDTO> list = bankService.findBanks(); // Returns ALL banks with balances
PageInfo<BankDTO> pageInfo = new PageInfo(list);
return GlobalResultGenerator.genSuccessResult(pageInfo);
}
}

Rify 挖掘思路

这类漏洞的通病是:Controller或方法上缺少鉴权注解,且方法内部也没有手动调用权限校验逻辑**。

我们可以编写一条组合规则,像筛子一样层层过滤:

// 1. 找到所有 Controller 类,排除掉本身就公开的(如 Auth, OpenData 等)
// 并且筛选出类定义上没有鉴权注解的
*Controller?{opcode:make}.__ref__?{!have:/(OpenData|OpenAi|Auth|Answer|LuceneSearch|Article|CommonApi)Controller/}?{!.annotation.*?{any:RequiresPermissions,RequiresRoles}} as $NotAuthClass
// 2. 获取这些类中的所有方法
$NotAuthClass.*?{opcode:function} as $func
// 3. 筛选出方法上也没有鉴权注解的
$func?{!.annotation.*?{any:RequiresPermissions,AuthorshipInterceptor}} as $NotAnnoAuthFunc
// 4. 最后,筛选出方法体内部也没有调用 "getCurrentUserByToken" (暗示手动鉴权) 的方法
$NotAnnoAuthFunc?{!<foreach_function_inst()>?{have:"getCurrentUserByToken"}} as $NotAnyAuthFunc
// 5. 输出结果
alert $NotAnyAuthFunc for { level:mid, message:"潜在的未授权访问 (CVE-2025-12924/12925)", risk:"越权" }

规则解析:

  • *Controller?{opcode:make}: 定位所有 Controller 类的定义。
  • !have:/(...)Controller/: 利用正则表达式排除白名单,减少误报。
  • .annotation: 检查注解。这里非常灵活,既查了类上的注解,也查了方法上的注解。
  • <foreach_function_inst()>: 深入方法内部,遍历每一条指令。
  • !have:"getCurrentUserByToken": 如果代码里连“获取当前用户”的操作都没有,大概率是没有做垂直越权检查的。

这条规则成功定位到了UserDicControllerBankController中那些“裸奔”的接口:

总结与思考:交互式挖掘,所想即所得

通过以上几个漏洞案例,我们可以清晰地看到:逻辑漏洞往往隐藏在特定的业务上下文中,需要审计人员敏锐的直觉来发现。

IRify配合 SyntaxFlow的最大价值,在于它为安全研究人员提供了一个极其流畅的交互式审计环境

1、极速的验证闭环:当你脑海中浮现出一个漏洞猜想(比如:“这里是不是没校验权限?”),你可以立刻在 IRify 中编写对应的 SyntaxFlow 规则。

2、现场调试与反馈:无需漫长的编译或数据库构建过程,规则写完即可运行。你能当场看到匹配结果,如果结果不准确,可以立即调整过滤条件,实时查看变化。

3、沉浸式逻辑梳理:这种“编写即运行、运行即反馈”的特性,让审计人员可以像写脚本一样,一步步逼近漏洞核心。

这种灵活编写、即时调试、当场见效的能力,极大地降低了将“审计思路”转化为“扫描规则”的门槛,让安全人员能够专注于挖掘业务逻辑背后的深层隐患,真正做到“所想即所得”。

附录:IRify 新版项目管理操作指南

为了提供更流畅的审计体验,IRify 对项目管理模块进行了底层重构。新版本不再以单次任务为维度,而是采用基于目录路径的项目``聚合逻辑。只要源代码路径一致,所有的编译和扫描记录都会归档在同一个项目下。

以下是新版界面的核心功能图解:

1. 常用操作一键直达

我们将最常用的四个核心功能提取到了项目列表页,您可以参考下图的数字索引进行操作:

  • ① 代码编译 (Compile)
  • ② 代码扫描 (Scan)
  • ③ 项目历史 (History)
  • ④ 编辑配置 (Edit)
  • ⑤ 删除(Delete)

2. 智能化的项目创建流程

“添加项目”不仅仅是填个表,它内置了智能探测逻辑:

  • 智能探测:您只需输入项目路径(支持本地绝对路径Git 、SVN远程链接),系统会自动探测项目语言,并自动配置过滤规则(排除无关文件),以避免无效编译影响性能。
  • 流程控制

3. 灵活的扫描策略

点击列表中的“扫描”按钮后,将进入扫描配置页:

  • 自动补全:如果该项目从未编译过,系统会默认拉取代码并进行首次编译。
  • 版本回溯:在编译历史下拉框中,您可以选择编译并扫描最新代码,也可以选择历史上任意一次编译版本**进行重扫。这意味着您可以随时用新写的规则去检测老版本的代码。
  1. 全面的历史回溯 (双维视图)

进入项目历史页面,我们将数据分为了左右两栏,逻辑更加清晰:

  • 左侧 - 编译历史:记录了代码的每一次变更版本。您可以在这里对旧版本的代码发起新的扫描或审计任务。
  • 右侧 - 扫描历史:对应左侧选中版本的每一次扫描结果。您可以在这里查看漏洞详情一键导出审计报告
  1. 旧项目迁移

如果您是从旧版本升级到新版的用户,可能会发现以前的扫描记录在列表中“消失”了。这是因为旧版的历史数据缺少新版项目管理所需的配置信息。在项目启动时候会提醒是否迁移旧项目数据,点击即可迁移;也可以在项目管理进行迁移。

功能作用:点击列表右上角的**“迁移旧项目数据”**按钮。

执行逻辑:系统会自动扫描数据库中那些没有关联项目配置的“孤儿”数据(即旧版产生的编译/扫描记录),并将它们识别、归档到新的项目视图中,确保您的历史资产不丢失。

6. 旧版入口说明

如果您仍需要访问旧版的列表视图,可以通过菜单栏的**“实验性功能 -> SSA项目编译历史”**进入。


本文首发于 Yak Project 公众号,阅读原文