1、前言
AI工作流系统在提升业务自动化效率的同时,其脚本执行、数据查询及权限控制等环节常存在高危缺陷。本文针对某开源系统进行代码审计,揭示攻击者可利用相关漏洞突破隔离边界、执行未授权操作、窃取核心数据甚至控制服务器,为同类平台的安全建设提供警示。
2、相关信息
spring-boot:3.5.9
mybatis-flex:1.11.6
3、代码审计流程
未授权漏洞
查看权限校验,Sa-Token的认证方式,拦截所有请求。
package tech.aiflowy.auth.config; import cn.dev33.satoken.interceptor.SaInterceptor; import cn.dev33.satoken.stp.StpUtil; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.annotation.Resource; @Configuration public class LoginAutoConfig implements WebMvcConfigurer { @Resource private LoginProperties properties; @Resource private NeedApiKeyInterceptor needApiKeyInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //创建Sa-Token拦截器,lambda表达式中调用StpUtil.checkLogin()检查用户是否已登录。 SaInterceptor saInterceptor = new SaInterceptor(handle -> { StpUtil.checkLogin(); }); registry.addInterceptor(new CurdInterceptor()) .order(101) .addPathPatterns("/**"); registry.addInterceptor(saInterceptor) .order(100) .addPathPatterns("/**") //设置优先级为100(数字越小等级越高),拦截除excludePathPatterns方法设置的路径外的全部请求 .excludePathPatterns("/") .excludePathPatterns("/error") .excludePathPatterns("/attachment/**") .excludePathPatterns("/api/v1/public/*") .excludePathPatterns("/api/v1/account/login") .excludePathPatterns("/api/v1/account/register") .excludePathPatterns("/thirdAuth/**") .excludePathPatterns("/public-api/**") .excludePathPatterns(properties.getExcludesOrEmpty()); registry.addInterceptor(needApiKeyInterceptor); } }在Sa-Token的认证机制下,以@SaIgnore用于声明一个接口或类不需要进行登录认证,示例如下,这里直接读取文件内容,但各种穿越符都尝试过了,不能穿越到重要文件的路径,这个以后再分析。
@SaIgnore @GetMapping("/api/images/**") public ResponseEntity<byte[]> getImage(HttpServletRequest request) throws IOException { // 获取完整的路径 String fullPath = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE); String basePath = "/api/images/"; String filePath = fullPath.substring(basePath.length()-1); String imagePath = getRootPath() + filePath; File imageFile = new File(imagePath); if (!imageFile.exists()) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } // 读取文件内容 byte[] fileContent = Files.readAllBytes(imageFile.toPath()); // 返回文件内容 return ResponseEntity.ok() .header(HttpHeaders.CONTENT_TYPE, getContentType(imageFile)) .body(fileContent); }寻找有价值的未授权端口,达到前台rce,/api/temp-token/create能够匿名创建临时token,但是经过测试每个功能点都会经过相对应的鉴权


代码执行
这里采取黑白盒的方式:本地搭建,查看对应功能点,寻找那种可以执行命令的功能点,再去查看相关的代码。(这也是新手最容易出洞的方式,其实也可以通过ai去审计全部代码,比较不好一点的是,ai太过大范围的搜索,反而会一无所获,如果核心代码逻辑打包成jar进行加载的话,ai不会主动去反编译)
我觉得这个时代下的代码审计,不必像传统的代码审计,通过简单的审计,反复调试,来寻找漏洞,但在深层次方面的漏洞,也不会普通的一句话,便让ai轻轻松松审计出来,更高效的方式,大概是告诉ai大致的审计思路、审计路线,让ai替你去实践。

审计流程如下:

GraalVM js 引擎,相关配置:参考文章:GraalVM Polyglot 沙箱逃逸:跨语言上下文 RCE-先知社区
static { CONTEXT_BUILDER = Context.newBuilder(new String[]{"js"}).option("engine.WarnInterpreterOnly", "false").allowHostAccess(HostAccess.ALL).allowHostClassLookup((className) -> false).option("js.ecmascript-version", "2021"); }1、HostAccess.ALL定义为允许 JS 访问所有注入 Java 对象的全部 public 方法和字段,包括 Object.getClass()、Class.getClassLoader() 等反射入口方法,这些方法是 JDK 基础类提供的,无法被 allowHostClassLookup 拦截。
2、className -> false永远返回 false,意图是禁止 Java.type("xxx")直接加载类。对通过已有对象实例调用 getClassLoader().loadClass()的反射路径完全无效。

效果:

后台sql注入漏洞
项目为 Spring Boot 3.5.9 多模块 Maven 工程,持久层框架为 MyBatis-Flex(mybatis-flex-spring-boot3-starter)
MyBatis-Flex 的危险 API 与原生 MyBatis 有所不同,审计重点锁定:
搜索 ${} 拼接(MyBatis 字符串替换)
grep -rn "\${" --include="*.xml" --include="*.java"搜索 wrapper.where(调用)
grep -rn "wrapper\.where(" --include="*.java"搜索 orderBy(直接拼接)
grep -rn "\.orderBy(" --include="*.java"可查找到src/main/java/tech/aiflowy/datacenter/service/impl/DatacenterTableServiceImpl.java,wrapper.where(condition)无任何处理,只需查询什么方法可以调用DatacenterQuery对象即可
private void buildCondition(QueryWrapper wrapper, DatacenterQuery where) { // 构建查询条件 String condition = where.getWhere(); if (StrUtil.isNotEmpty(condition)) { wrapper.where(condition); } }grep -rn "DatacenterQuery" --include="*.java"就有:src/main/java/tech/aiflowy/admin/controller/datacenter/DatacenterTableController.java
@GetMapping("/getPageData") @SaCheckPermission("/api/v1/datacenterTable/query") public Result<Page<Row>> getPageData(DatacenterQuery where) { Page<Row> res = service.getPageData(where); return Result.ok(res); }payload构造:src/main/java/tech/aiflowy/common/entity/DatacenterQuery.java设置了需要的参数pageNumber、pageSize、tableId、where(注入点):
package tech.aiflowy.common.entity; import java.math.BigInteger; public class DatacenterQuery { private Long pageNumber; private Long pageSize; // 表ID private BigInteger tableId; // 工作流传过来的查询条件 private String where; public Long getPageNumber() { return pageNumber; } public void setPageNumber(Long pageNumber) { this.pageNumber = pageNumber; } public Long getPageSize() { return pageSize; } public void setPageSize(Long pageSize) { this.pageSize = pageSize; } public BigInteger getTableId() { return tableId; } public void setTableId(BigInteger tableId) { this.tableId = tableId; } public String getWhere() { return where; } public void setWhere(String where) { this.where = where; } }需要在相应的模块中创建数据表:

效果:

夜雨聆风