Kubernetes Goat Internal API 组件深度解析:从镜像构建到 SSRF 实战利用
【免费下载链接】kubernetes-goatKubernetes Goat is a "Vulnerable by Design" cluster environment to learn and practice Kubernetes security using an interactive hands-on playground 🚀项目地址: https://gitcode.com/GitHub_Trending/ku/kubernetes-goat
Kubernetes Goat 是一个"故意设计为存在漏洞"(Vulnerable by Design)的 Kubernetes 集群环境,用于在交互式沙箱中学习与演练 Kubernetes 安全。Internal API(internal-api)正是该环境中的一个核心攻击面组件:它以 Node.js + Express 实现了一个脆弱的"内部 API 代理"服务,通过把用户输入直接拼进curl子进程执行,为 SSRF(服务端请求伪造)等云原生攻击提供了真实可复现的靶点。阅读本文后,你将完整掌握该组件的镜像构建与发布流程、源码级漏洞原理、集群内部署方式,以及在 Kubernetes Goat 场景中的实战利用路径。
组件定位:Kubernetes Goat 中的内部代理攻击面
Internal API 是 Kubernetes Goat 基础设施(infrastructure/ 目录)中的一个 Docker 容器组件。其镜像名为madhuakula/k8s-goat-internal-api,是一个运行在集群内部的 HTTP 代理服务:外部攻击者(或场景学习者)可以向它提交任意的 URL 端点、HTTP 方法与自定义请求头,由它代为发起请求并返回响应。
从集群网络拓扑来看,它承担着两层角色:
- 集群内微服务代理:它能够访问 Kubernetes 集群内部网络,是探测内网服务(如
metadata-db等仅集群内可达的 Service)的跳板; - SSRF 漏洞载体:其实现方式决定了它天然存在服务端请求伪造问题,这正是场景 3(SSRF 到云元数据/内网服务)的核心利用入口。
关联文档 infrastructure/internal-api/README.md 明确说明"此 Docker 容器是 Kubernetes Goat 的一部分",并给出了镜像构建与推送的标准流程。
镜像构建与发布流程
关联文档给出了该组件的两个核心镜像运维命令,二者均为标准的 Docker 工作流:
docker build -t madhuakula/k8s-goat-internal-api .该命令在 infrastructure/internal-api/ 目录下执行,会依据该目录中的 Dockerfile 构建镜像。构建完成后,可以将镜像推送到镜像仓库供集群拉取:
docker push madhuakula/k8s-goat-internal-api该命令将madhuakula/k8s-goat-internal-api镜像推送到 Docker Hub。值得注意的是,整个 Kubernetes Goat 的部署(setup-kubernetes-goat.sh)默认依赖这些预构建的公开镜像直接拉取使用,因此本地重新构建镜像通常仅在定制或离线场景下才需要。
构建细节:Dockerfile 剖析
infrastructure/internal-api/Dockerfile 展示了镜像的完整构建过程:
FROM node:alpine LABEL MAINTAINER="Madhu Akula" INFO="Kubernetes Goat" WORKDIR /usr/src/app COPY code/package*.json ./ RUN npm install \ && apk add --no-cache curl COPY code/* ./ EXPOSE 3000 CMD [ "npm", "start" ]几个关键点:
- 基础镜像:使用
node:alpine,体积小巧,适合作为工具型容器; - 显式安装
curl:通过apk add --no-cache curl在 Alpine 中安装 curl。这是漏洞成立的必要前提——server.js 正是通过spawnSync('curl', ...)来发起请求的,如果容器内没有 curl,整个代理功能将不可用; - 端口约定:
EXPOSE 3000,与源码中server.listen(3000)一致,也与集群 Service 的targetPort: 3000对应; - 启动方式:
CMD [ "npm", "start" ],对应 package.json 中"start": "node server.js"脚本。
依赖清单
package.json 声明了运行时依赖:
"dependencies": { "body-parser": "^1.20.1", "express": "^4.19.2" }仅有两个依赖:Express 4 负责 HTTP 路由,body-parser 负责解析 POST 请求体(JSON 与 urlencoded 两种格式,见下节源码)。npm start启动入口为node server.js。
源码级漏洞原理剖析
服务端实现:一个不设防的代理
infrastructure/internal-api/code/server.js 是全部业务逻辑所在,全文不足 50 行:
const http = require('http'); const express = require('express'); const path = require('path'); const bodyParser = require('body-parser'); const { spawnSync } = require('child_process'); const app = express(); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(express.json()); app.use(express.static("express")); app.get('/', function (req, res) { res.sendFile(path.join(__dirname + '/index.html')); }); app.post('/', function (req, res) { var endpoint = req.body.endpoint, method = req.body.method || 'GET', headers = req.body.headers || {}; const child = spawnSync('curl', [endpoint, '-H', headers, '-X', method]); if (child.stdout) { res.send(child.stdout) } else if (child.err) { res.send(child.err) } else { res.send(child.stderr) } // ...被注释掉的 fetch 实现,见下文 }); const server = http.createServer(app); const port = 3000; server.listen(port); console.debug('Server listening on port ' + port);逐行分析可以提炼出三个核心机制:
1. 请求解析(body-parser)
应用同时启用了body-parser.urlencoded、body-parser.json和express.json(),意味着前端既可以提交表单编码数据,也可以提交 JSON 数据(前端实际使用的是 JSON,见 index.html 的fetch调用)。
2. 关键漏洞:spawnSync('curl', ...)命令拼接
POST /处理器从请求体中取出三个字段:
endpoint:目标 URL(无任何协议白名单、域名校验或 SSRF 防护);method:HTTP 方法,默认GET;headers:自定义请求头。
随后这些值被原样拼入spawnSync('curl', [endpoint, '-H', headers, '-X', method])执行。这里存在两类问题:
- SSRF(服务端请求伪造):
endpoint完全由攻击者控制,服务端 curl 可访问:- 云厂商实例元数据服务,例如
http://169.254.169.254/latest/meta-data/(AWS 等平台)——这是场景 3 中访问云元数据的关键路径; - 集群内部 DNS 可达的服务,例如
http://metadata-db、http://metadata-db/latest/secrets/kubernetes-goat——仅集群内可达的 Service 也会被代理访问; - 容器同 Pod 内其他容器、节点网络上的任意地址。
- 云厂商实例元数据服务,例如
- 命令注入风险面:
headers参数被直接作为-H参数传入,虽然没有经过 shell 解释(spawnSync默认以数组参数形式调用,不经过 shell),不存在直接的 shell 命令注入,但从安全审计角度,将不可信输入传入子进程始终是高风险模式。
3. 输出盲区
响应处理逻辑存在一个有趣的缺陷:
if (child.stdout) { res.send(child.stdout) } else if (child.err) { res.send(child.err) } else { res.send(child.stderr) }分支判断顺序依次为stdout、err、stderr。其中child.err在 Node.js 的spawnSync返回对象中并非标准字段(标准字段是error),因此实际上当 stdout 为空、stderr 有内容时,才可能走到res.send(child.stderr)分支。从源码结构可以推断,这属于刻意/无意保留的宽松处理,保证绝大多数请求响应(无论成功与否)都会回传,方便攻击者直接看到代理结果。
被注释掉的"正确"实现
server.js 末尾保留了一段被注释掉的fetch实现:
// fetch(endpoint, { // method: method, // headers: headers, // }) // .then(res => res.json()) // .then(json => res.send(json)) // .catch(json => res.send(json));从源码结构看,这段注释暗示了作者对"通过 curl 子进程发起请求"与"通过 Node.js fetch 发起请求"两种方案的取舍——无论哪种方案,只要不对endpoint做校验,SSRF 漏洞都依然存在。最终采用spawnSync('curl')的实现更贴近真实世界中"为了省事直接调用系统工具"的脆弱代码风格,这正是本场景希望传递的教学信息。
前端交互界面
infrastructure/internal-api/code/index.html 是一个基于 Tailwind CSS 的单页界面,标题为 "Internal API Proxy Service",包含三个输入框:
| 输入项 | 占位符示例 | 提交后映射到 |
|---|---|---|
| Endpoint | https://api.github.com | req.body.endpoint |
| Method | GET | req.body.method |
| Custom Header | Content-Type: application/json | req.body.headers |
页面底部的 JavaScript 通过fetch('/', { method: "POST", ... })将三个字段以 JSON 形式提交到服务端,并把响应文本渲染到 "Response Output" 区域。值得注意的是,默认占位符直接展示了https://api.github.com这样的公网地址,说明设计意图是让使用者自由输入任意 URL——包括内网地址与元数据地址。
集群内部署:Deployment 与 Service
该组件在集群中的部署清单位于 scenarios/internal-proxy/deployment.yaml,由 Kubernetes Goat 的安装脚本自动应用:
kubectl apply -f scenarios/internal-proxy/deployment.yaml(对应 setup-kubernetes-goat.sh 中的安装步骤。)
多容器 Pod 设计
该 Deployment(名为internal-proxy-deployment)采用 sidecar 模式,一个 Pod 内运行两个容器:
| 容器 | 镜像 | 端口 | 资源限制(limits) |
|---|---|---|---|
internal-api | madhuakula/k8s-goat-internal-api | 3000 | cpu 50m / memory 60Mi |
info-app | madhuakula/k8s-goat-info-app | 5000 | cpu 30m / memory 50Mi |
info-app容器(对应 infrastructure/info-app/)与 internal-api 共享同一个 Pod 网络命名空间,因此通过代理访问http://127.0.0.1:5000即可触达同 Pod 内的 info-app 服务——这正是场景 3 中"探测同容器其他端口服务"的第一步(见下文实战部分)。
三种暴露方式
清单中定义了三个 Kubernetes 对象,覆盖了不同的访问层级:
- Deployment
internal-proxy-deployment:selector 标签为app: internal-proxy,负责 Pod 编排; - ClusterIP Service
internal-proxy-api-service:端口 3000 → targetPort 3000,仅集群内可达,供 SSRF 场景中由攻击者借道访问; - NodePort Service
internal-proxy-info-app-service:端口 5000 → targetPort 5000,nodePort: 30003,将 info-app 暴露到集群节点端口,便于直接访问验证。
其中 internal-api 本体没有直接暴露 NodePort,对外访问依赖 access-kubernetes-goat.sh 中的端口转发:
export POD_NAME=$(kubectl get pods --namespace default -l "app=internal-proxy" -o jsonpath="{.items[0].metadata.name}") kubectl port-forward $POD_NAME --address 0.0.0.0 1232:3000 > /dev/null 2>&1 &即将 Pod 的 3000 端口转发到本机 1232 端口(对应 access-kubernetes-goat.sh),使学习者通过浏览器访问http://127.0.0.1:1232即可打开 Internal API Proxy 界面。
实战场景:SSRF 到云元数据与集群内服务
在 Kubernetes Goat 中,internal-api 组件是场景 3(SSRF)的主战场。该场景的完整演练文档位于 guide/docs/scenarios/scenario-3/scenario-3.md,其核心故事是:利用应用漏洞(SSRF)访问云实例元数据以及集群内部服务元数据信息。
场景入口与目标
- 场景入口:通过端口转发访问
http://127.0.0.1:1232,即 Internal API Proxy 界面; - 场景目标:在 metadata secrets 中获取
k8s-goat-FLAG标志值。
攻击路径三步走
第一步:探测同 Pod 内其他服务
利用代理访问http://127.0.0.1:5000(方法 GET),可以确认同 Pod 内的info-app服务正在运行并返回 HTTP 响应。这一步验证了 Pod 网络命名空间共享这一 Kubernetes 特性,同时为后续扩大攻击面提供了信息基础。
第二步:借道集群 DNS 访问内网 Service
Kubernetes 原生服务发现为攻击者提供了便利:任意 Service 都可以通过servicename.namespace.svc.cluster.local或简写形式(同命名空间下直接用服务名)在集群内被解析。利用代理访问http://metadata-db,可以触达一个仅集群内可达的元数据微服务。
第三步:枚举密钥并解码标志
对metadata-db返回的键值逐层枚举后,最终在http://metadata-db/latest/secrets/kubernetes-goat端点发现 Base64 编码的标志值:
echo -n "azhzLWdvYXQtY2E5MGVmODVkYjdhNWFlZjAxOThkMDJmYjBkZjljYWI=" | base64 -d解码后可得到明文标志(格式为k8s-goat-*)。metadata-db组件的实现见 infrastructure/metadata-db/(一个用 Go 编写的模拟云元数据服务的微服务)。
云元数据探测(可选分支)
场景文档同时指出:169.254.169.254是各主流云厂商(AWS、GCP、Azure、Digital Ocean 等)提供的实例元数据地址。若集群运行在真实云环境上,攻击者可以继续利用代理访问http://169.254.169.254/latest/meta-data/获取实例元数据;若运行在本地(如 kind、minikube),则跳过此步,专注于集群内部服务枚举。
安全启示与加固建议
从该组件的实现可以提炼出对真实世界云原生应用的安全启示:
- 任何转发/代理类服务都必须做目标校验:对
endpoint实施协议白名单(仅允许 http/https)、域名/IP 黑名单(封锁169.254.169.254等链路本地地址、集群内网网段、metadata-db等敏感服务名),或维护显式的允许列表; - 避免将不可信输入直接拼入子进程命令:即使使用
spawnSync数组形式(不经 shell)规避了直接的命令注入,也应优先使用语言内建的 HTTP 客户端,并对重定向(curl 的-L)与 DNS 重绑定攻击保持警惕; - 最小化暴露面:internal-api 仅通过 ClusterIP 在集群内暴露,外部依赖临时
port-forward访问——这种"内网服务不直接暴露到公网"的默认配置正是值得在真实环境中推广的做法。
Kubernetes Goat 的安全扫描报告(如 guide/docs/security-reports/checkov.md)中,针对internal-proxy/deployment.yaml也列出了多项 Checkov 检查项(包括缺少安全上下文、未固定镜像标签、使用默认命名空间、缺少存活/就绪探针等),可以作为进一步加固该部署清单的参考清单。
小结
Internal API 组件虽然代码量极小,却浓缩了 Kubernetes Goat 的核心教学价值:一个真实风格的脆弱服务如何在集群网络中被一步步利用——从容器内同 Pod 探测,到 Kubernetes 服务发现,再到云元数据访问,最终拿到标志值。理解其镜像构建、源码实现与部署清单,你就能在本地复现完整的 SSRF 攻防链路,并将同样的防护意识迁移到真实业务中。继续深入可以查看 场景 3 完整文档 以及同系列的 场景 16(RBAC 越权),后者展示了从该服务账户出发的另一种提权路径。
【免费下载链接】kubernetes-goatKubernetes Goat is a "Vulnerable by Design" cluster environment to learn and practice Kubernetes security using an interactive hands-on playground 🚀项目地址: https://gitcode.com/GitHub_Trending/ku/kubernetes-goat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考