最近做代码审计,遇到一个SpringCloud系统存在漏洞,找到了Controller,但是不会发请求包,于是通过RuoYi-Cloud来学一下怎么构造请求路径
环境搭建 参考链接 https://doc.ruoyi.vip/ruoyi-cloud/document/hjbs.html
搭建环境
RuoYi-Cloud v3.6.6,自行克隆代码checkout
JDK 17.0.16
Windows x64
Kali
拉取必要镜像
1 2 3 docker pull mysql:5.7 docker pull redis:8 docker pull nacos/nacos-server:v3.1.0
MySQL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 rm -rf /data/mysql/{data,conf,logs}mkdir -p /data/mysql/{data,conf,logs}mkdir -p /data/mysql/initdocker run -d \ --name mysql57 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root \ -e TZ=Asia/Shanghai \ -v /data/mysql/data:/var/lib/mysql \ -v /data/mysql/conf:/etc/mysql/conf.d \ -v /data/mysql/logs:/var/log/mysql \ -v /etc/localtime:/etc/localtime \ mysql:5.7 \ --character-set-server=utf8mb4 \ --collation-server=utf8mb4_unicode_ci \ --default-time-zone=+8:00
创建容器后,使用Navicat连接,自行执行SQL语句
随后将ry-config数据中config_info表下关于Redis和MySQL配置,修改成正确的连接地址和密码(我这里是Docker服务器,即Linux上的)
Redis 1 2 3 4 5 6 7 8 9 10 rm -rf /data/redis/{data,conf}mkdir -p /data/redis/{data,conf}docker run -d \ --name redis8 \ -p 6379:6379 \ -e TZ=Asia/Shanghai \ -v /data/redis/data:/data \ -v /data/redis/conf/redis.conf:/usr/local/etc/redis/redis.conf \ redis:8 \ redis-server /usr/local/etc/redis/redis.conf
redis.conf需修改下方配置
1 2 3 bind * -::* requirepass redis appendonly yes
默认配置文件地址 https://github.com/redis/redis/blob/unstable/redis.conf
nacos 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 docker run -d \ --name nacos \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ --privileged=true \ -e MODE=standalone \ -e TZ=Asia/Shanghai \ -e NACOS_AUTH_TOKEN=bHJ1aTFscnVpMWxydWkxbHJ1aTFscnVpMWxydWkxbHJ1aTE= \ -e NACOS_AUTH_IDENTITY_KEY=lrui2 \ -e NACOS_AUTH_IDENTITY_VALUE=bHJ1aTJscnVpMmxydWkybHJ1aTJscnVpMmxydWkybHJ1aTI= \ -e SPRING_DATASOURCE_PLATFORM=mysql \ -e MYSQL_SERVICE_HOST=192.168.19.131 \ -e MYSQL_SERVICE_PORT=3306 \ -e MYSQL_SERVICE_DB_NAME=ry-config \ -e MYSQL_SERVICE_USER=root \ -e MYSQL_SERVICE_PASSWORD=root \ nacos/nacos-server:v3.1.0
相关秘钥描述如下
1 2 3 4 5 6 7 8 9 10 lrui1lrui1lrui1lrui1lrui1lrui1lrui1 base64 -> bHJ1aTFscnVpMWxydWkxbHJ1aTFscnVpMWxydWkxbHJ1aTE=lrui2 lrui2lrui2lrui2lrui2lrui2lrui2lrui2 base64 -> bHJ1aTJscnVpMmxydWkybHJ1aTJscnVpMmxydWkybHJ1aTI=
启动Nacos前,需要启动MySQL,让Nacos去加载MySQL里的数据
后端 IDEA运行下方的必要模块(启动顺序不影响)
RuoYiGatewayApplication (网关模块 必须)
RuoYiAuthApplication (认证模块 必须)
RuoYiSystemApplication (系统模块 必须)
前端 1 2 3 4 5 6 7 8 9 10 11 cd ruoyi-uinpm install npm install --registry=https://registry.npmmirror.com npm run dev
一些知识点 RuoYiCloud用户请求的流转过程 用户访问 http://192.168.1.2/dev-api/system/user/getInfo ,前端和后端的处理顺序如下
请求路径以/dev-api开头,触发前端的反向代理,去掉前缀后,向后端(网关)请求/system/user/getInfo
8080端口网关接收到/system/user/getInfo请求,根据MySQL读取的ruoyi-gateway-dev.yml配置文件中,对于路由的定义
1 2 3 4 5 6 7 - id: ruoyi-system uri: lb://ruoyi-system predicates: - Path=/system/** filters: - StripPrefix=1
符合以/system开头的条件,网关将其转发到ruoyi-system服务,让其使用负载均衡分配服务器完成相应;最后使用StripPrefix过滤器去掉第一个路径,实际转发的路径为/user/getInfo 3. ruoyi-system模块接收到请求后,根据/user/getInfo找到对应的Controller,进行业务处理
鉴权 RuoYi-Cloud的鉴权主要通过RuoYi-Cloud\ruoyi-gateway\src\main\java\com\ruoyi\gateway\filter\AuthFilter,因其实现GlobalFilter接口,对经过网关的所有请求做鉴权,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Override public Mono<Void> filter (ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest.Builder mutate = request.mutate(); String url = request.getURI().getPath(); if (StringUtils.matches(url, ignoreWhite.getWhites())) { return chain.filter(exchange); } String token = getToken(request); if (StringUtils.isEmpty(token)) { return unauthorizedResponse(exchange, "令牌不能为空" ); } Claims claims = JwtUtils.parseToken(token); if (claims == null ) { return unauthorizedResponse(exchange, "令牌已过期或验证不正确!" ); } String userkey = JwtUtils.getUserKey(claims); boolean islogin = redisService.hasKey(getTokenKey(userkey)); if (!islogin) { return unauthorizedResponse(exchange, "登录状态已过期" ); } String userid = JwtUtils.getUserId(claims); String username = JwtUtils.getUserName(claims); if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) { return unauthorizedResponse(exchange, "令牌验证失败" ); } addHeader(mutate, SecurityConstants.USER_KEY, userkey); addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid); addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username); removeHeader(mutate, SecurityConstants.FROM_SOURCE); return chain.filter(exchange.mutate().request(mutate.build()).build()); }
除了上方的全局过滤器,还可以使用了自定义注解和切面鉴权,切面代码如下
RuoYi-Cloud\ruoyi-common\ruoyi-common-security\src\main\java\com\ruoyi\common\security\aspect\PreAuthorizeAspect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 @Aspect @Component public class PreAuthorizeAspect { public PreAuthorizeAspect () { } public static final String POINTCUT_SIGN = " @annotation(com.ruoyi.common.security.annotation.RequiresLogin) || " + "@annotation(com.ruoyi.common.security.annotation.RequiresPermissions) || " + "@annotation(com.ruoyi.common.security.annotation.RequiresRoles)" ; @Pointcut(POINTCUT_SIGN) public void pointcut () { } @Around("pointcut()") public Object around (ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); checkMethodAnnotation(signature.getMethod()); return joinPoint.proceed(); } public void checkMethodAnnotation (Method method) { RequiresLogin requiresLogin = method.getAnnotation(RequiresLogin.class); if (requiresLogin != null ) { AuthUtil.checkLogin(); } RequiresRoles requiresRoles = method.getAnnotation(RequiresRoles.class); if (requiresRoles != null ) { AuthUtil.checkRole(requiresRoles); } RequiresPermissions requiresPermissions = method.getAnnotation(RequiresPermissions.class); if (requiresPermissions != null ) { AuthUtil.checkPermi(requiresPermissions); } } }
其他 1、具体有多少个service,取决于@SpringBootApplication的注解有多少个,有多少个@SpringBootApplication就有多少个service
2、lb是负载均衡load balancing的缩写。lb:// 是 Spring Cloud(Gateway 或 LoadBalancer)层面的 URI 规则 ,用于向服务发现组件(如 Nacos)注册的服务做负载均衡调用。lb://service-name 是 Spring Cloud 用于告诉调用方进行负载均衡
3、关于dev-api,前端服务器请求后端时,使用pathRewrite已经去掉了
参考 https://gitee.com/y_project/RuoYi-Cloud/issues/I38T7R
关于配置文件 服务加载配置文件时,配置了nacos.config,它就会去nacos加载
1 2 3 4 5 spring: cloud: nacos: config: server-addr: ...
spring.application.name + spring.profiles.active + file-extension的配置文件
在RuoYiGatewayApplication服务中,bootstrap.yml配置如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 server: port: 9201 spring: application: name: ruoyi-system profiles: active: dev cloud: nacos: discovery: server-addr: kali:8848 config: server-addr: kali:8848 file-extension: yml shared-configs: - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
由于配置了nacos.config,网关服务会向Nacos请求配置文件,分别是
ruoyi-gateway-dev.yml 默认
application-dev.yml 共享
Nacos中配置文件的是从MySQL加载的
这边简单介绍下ruoyi-gateway-dev.yml中有关路由的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 spring: cloud: gateway: discovery: locator: lowerCaseServiceId: true enabled: true routes: - id: ruoyi-auth uri: lb://ruoyi-auth predicates: - Path=/auth/** filters: - CacheRequestBody - ValidateCodeFilter - StripPrefix=1 - id: ruoyi-gen uri: lb://ruoyi-gen predicates: - Path=/code/** filters: - StripPrefix=1 - id: ruoyi-job uri: lb://ruoyi-job predicates: - Path=/schedule/** filters: - StripPrefix=1 - id: ruoyi-system uri: lb://ruoyi-system predicates: - Path=/system/** filters: - StripPrefix=1 - id: ruoyi-file uri: lb://ruoyi-file predicates: - Path=/file/** filters: - StripPrefix=1
下方来自ChatGPT老师(懒得翻文档了)
discovery.locator
spring.cloud.gateway.discovery.locator.enabled: true 启用基于服务发现的路由自动注册(DiscoveryClientRouteDefinitionLocator)。当为 true 时,网关会从服务发现(Eureka / Nacos / Consul 等)自动加载服务并生成路由,通常会把服务名映射到 lb://<SERVICE> 的 URI。
lowerCaseServiceId: true 将发现的服务 ID 转为小写后用于生成路由或匹配(例如服务发现返回 RUOYI-AUTH,会变为 ruoyi-auth)。这在服务名大小写不一致时避免匹配问题。
注意:启用发现 locator 会动态产生大量路由,且与下面显式 routes 配置会一起生效(显式优先,具体按 Spring Cloud Gateway 版本)。
routes 显式定义的一组路由(手动配置)。每个 - id: <id> 定义一条路由。
通用字段解释:
id:路由 id(用于日志/调试)。
uri:目标 URI。以 lb:// 开头表示“基于负载均衡”的 URI,网关会通过 Spring Cloud LoadBalancer(或 Ribbon)把请求转发给注册在服务发现中的实例,例如 lb://ruoyi-auth 表示把请求转到服务名为 ruoyi-auth 的实例集群。
predicates:一组谓词(条件),只有当请求满足某个谓词时该路由才会匹配。常见 Path=/auth/** 表示请求路径匹配 /auth/**。
filters:该路由在转发前/后执行的一组过滤器,用于修改请求/响应、鉴权、日志等。
其他 1、StripPrefix=1 这个过滤器把请求路径的第一个路径段去掉
/auth/login 去掉后,对应auth服务接收到 /login
2、ValidateCodeFilter路径在 RuoYi-Cloud\ruoyi-gateway\src\main\java\com\ruoyi\gateway\filter\ValidateCodeFilter。处理验证码的过滤器
3、CacheRequestBody 缓存请求体 https://docs.springframework.org.cn/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories/cacherequestbody-factory.html
总结 对于SpringCloud+Nacos的微服务项目,请求路径受Controller+网关路由+前端反代影响。相对于SpringBoot来说多了一层网关路由