CTFZone2018-Federation Workflow System

翻译自https://github.com/p4-team/ctf/tree/master/2018-07-21-ctfzone-quals/crypto_federation
题目描述

The source code for the Federation Workflow System has been leaked online this night.
Our goal is to inspect it and gain access to their Top Secret documents.
nc crypto-04.v7frkwrfyhsjtbpfcppnu.ctfz.one 7331
考察的是在一些条件下,对AES-ECB的攻击。
给了server.py 和 client.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import sys
import hmac
import socket
from hashlib import sha1
from Crypto.Cipher import AES
from struct import pack, unpack
from threading import Thread, Lock
from base64 import standard_b64encode
from time import time, sleep, strftime


class SecureServer:

    def __init__(self):
        self.msg_end = '</msg>'
        self.msg_not_found = 'NOT_FOUND'
        self.msg_wrong_pin = 'BAD_PIN'
        self.lock = Lock()
        self.log_path = '../top_secret/server.log'
        self.real_flag = '../top_secret/real.flag'
        self.aes_key = '../top_secret/aes.key'
        self.totp_key = 'totp.secret'
        self.files_available = [
                                    'lorem.txt',
                                    'flag.txt',
                                    'admin.txt',
                                    'password.txt'
                                ]

        self.host = '0.0.0.0'
        self.port = 7331
        self.buff_size = 1024

        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.sock.bind((self.host, self.port))
        self.sock.listen(50)

        self.listener = Thread(target=self.listen)
        self.listener.daemon = True
        self.listener.start()

        self.log('Server started')

    def listen(self):
        while True:
            try:
                client, address = self.sock.accept()
                client.settimeout(30)
                sock_thread = Thread(target=self.handle, args=(client, address))
                sock_thread.daemon = True
                sock_thread.start()

                self.log('Client {0} connected'.format(address[0]))

            except Exception as ex:
                self.log(ex)

    def handle(self, client, address):
        data = self.recv_until(client, self.msg_end)
        self.log('Got message from client {0}: {1}'.format(address[0], data))

        args = data.split(' ', 1)
        command = args[0].strip()

        if command == 'list':
            self.send_list_files(client, address)
        elif command == 'login':
            self.send_login_time(client, address)
        elif command == 'file':
            if len(args) != 2:
                self.send(client, 'Bad request')
            else:
                self.send_file_data(args[1], client, address)
        elif command == 'admin':
            if len(args) != 2:
                self.send(client, 'Bad request')
            else:
                self.send_admin_token(args[1], client, address)
        else:
            self.send(client, 'Bad request or timed out')

        client.close()

    def send_list_files(self, client, address):
        self.send(client, ','.join(self.files_available))
        self.log('Sending available files list to client {0}'.format(address[0]))

    def send_login_time(self, client, address):
        self.send(client, int(time()))
        self.log('Client auth from {0}'.format(address[0]))

    def send_file_data(self, file, client, address):
        content = self.read_file(file)
        response = '{0}: {1}'.format(file, content)
        encrypted_response = self.encrypt(response)
        self.send(client, encrypted_response)
        self.log('Sending file "{0}" to client {1}'.format(file, address[0]))

    def send_admin_token(self, client_pin, client, address):
        try:
            if self.check_totp(client_pin):
                response = 'flag: {0}'.format(open(self.real_flag).read())
                self.send(client, response)
                self.log('Sending admin token to client {0}'.format(address[0]))
            else:
                self.send(client, self.msg_wrong_pin)
                self.log('Wrong pin from client {0}'.format(address[0]))

        except Exception as ex:
            self.log(ex)
            self.send(client, 'Bad request')

    def check_totp(self, client_pin):
        try:
            secret = open(self.totp_key).read()
            server_pin = self.totp(secret)
            return client_pin == server_pin

        except Exception as ex:
            self.log(ex)
            return False

    def totp(self, secret):
        counter = pack('>Q', int(time()) // 30)
        totp_hmac = hmac.new(secret.encode('UTF-8'), counter, sha1).digest()
        offset = totp_hmac[19] & 15
        totp_pin = str((unpack('>I', totp_hmac[offset:offset + 4])[0] & 0x7fffffff) % 1000000)
        return totp_pin.zfill(6)

    def encrypt(self, data):
        block_size = 16

        data = data.encode('utf-8')
        pad = block_size - len(data) % block_size
        data = data + (pad * chr(pad)).encode('utf-8')

        key = open(self.aes_key).read()
        cipher = AES.new(key, AES.MODE_ECB)

        return standard_b64encode(cipher.encrypt(data)).decode('utf-8')

    def read_file(self, file):
        try:
            clean_path = self.sanitize(file)
            if clean_path is not None:
                return open(clean_path).read()
            else:
                return self.msg_not_found

        except Exception as ex:
            self.log(ex)
            return self.msg_not_found

    def sanitize(self, file):
        try:
            if file.find('\x00') == -1:
                file_name = file
            else:
                file_name = file[:file.find('\x00')]

            file_path = os.path.realpath('files/{0}'.format(file_name))

            if file_path.startswith(os.getcwd()):
                return file_path
            else:
                return None

        except Exception as ex:
            self.log(ex)
            return None

    def send(self, client, data):
        client.send('{0}{1}'.format(data, self.msg_end).encode('UTF-8'))

    def recv_until(self, client, end):
        try:
            recv = client.recv(self.buff_size).decode('utf-8')
            while recv.find(end) == -1:
                recv += client.recv(self.buff_size).decode('utf-8')
            return recv[:recv.find(end)]

        except Exception as ex:
            self.log(ex)
            return ''

    def log(self, data):
        self.lock.acquire()
        print('[{0}] {1}'.format(strftime('%d.%m.%Y %H:%M:%S'), data))
        sys.stdout.flush()
        self.lock.release()


if __name__ == '__main__':
    secure_server = SecureServer()

    while True:
        try:
            sleep(1)
        except KeyboardInterrupt:
            secure_server.log('Server terminated')
            exit(0)
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import socket
from time import sleep
from Crypto.Cipher import AES
from base64 import standard_b64decode


class SecureClient:

    def __init__(self):
        self.msg_end = '</msg>'
        self.msg_wrong_pin = 'BAD_PIN'
        self.aes_key = 'aes.key'

        self.host = 'crypto-04.v7frkwrfyhsjtbpfcppnu.ctfz.one'
        self.port = 7331
        self.buff_size = 1024

        try:
            self.greeting()
        except KeyboardInterrupt:
            exit(0)

    def greeting(self):
        self.cls()

        print('\n   ==================================== !!! CONFIDENTIALITY NOTICE !!! ====================================')
        print('   ||                 You trying to access high confidential Federation workflow system.                 ||')
        print('   ||                 If you are not authorised to use this system leave it immediately.                 ||')
        print('   || Otherwise incident will be reported and you will be eliminated as it considered by Federation Law. ||')
        print('   ========================================================================================================\n')
        user_choice = input('   Do you want to proceed? (yes/no) > ')

        if user_choice.lower() == 'yes':
            print('   Checking user...')
            sleep(5)
            print('   SUCCESS: ACCESS GRANTED')
            print('   Last login time: {0}'.format(self.get_last_login()))
            sleep(1)
            self.cls()
            print('\n   Welcome, Head Consul.')
            self.main_menu()

        else:
            print('   Checking user...')
            sleep(5)
            print('   ERROR: UNAUTHORISED USER')
            sleep(1)

            print('\n   Reporting incident...')
            sleep(5)
            print('   SUCCESS: INCIDENT REPORTED')
            sleep(1)

            print('\n   Please stay in place and wait for Federation Security Department extraction team.\n')
            exit(0)

    def main_menu(self):
        while True:
            print("\n   You are authorised to:")
            print("      list - view list of available files")
            print("      file - request file from server")
            print("      admin - use administrative functions")
            print("      exit - exit workflow system")

            user_choice = input('\n   What do you want to do? (list/file/admin/exit) > ')

            self.cls()

            if user_choice.lower() == 'list':
                self.list_files()
            elif user_choice.lower() == 'file':
                self.view_file()
            elif user_choice.lower() == 'admin':
                self.admin()
            elif user_choice.lower() == 'exit':
                exit(0)
            else:
                print('\n   Unrecognized command, try again')

    def list_files(self):
        file_list = self.get_file_list()

        print('\n   You are authorised to view listed files:\n')
        for file in file_list:
            print('   - {0}'.format(file))

    def view_file(self):
        self.list_files()

        filename = input('\n   Which file you want to view? > ')
        file_content = self.send('file {0}'.format(filename))

        if len(file_content) > 0:
            plain_content = self.decrypt(file_content)
            if len(plain_content) > 0:
                print('\n   ========================================================================================================')
                print('   Content of {0}'.format(plain_content))
                print('   ========================================================================================================')
            else:
                print('\n   Seems like you have no decryption key, so you can\'t see any files.')
        else:
            print('\n   Error while requesting file')

    def admin(self):
        print('\n   Access to administrative functions requires additional security check.')
        pin = input('   Enter your administrative PIN > ')
        response = self.send('admin {0}'.format(pin))

        if response == self.msg_wrong_pin:
            print('\n   Wrong administrative PIN. Incident will be reported.')
        else:
            print('\n   High confidential administrative data: {0}'.format(response))

    def decrypt(self, data):
        try:
            key = open(self.aes_key).read()
            cipher = AES.new(key, AES.MODE_ECB)

            plain = cipher.decrypt(standard_b64decode(data)).decode('UTF-8')
            plain = plain[:-ord(plain[-1])]
            return plain

        except Exception as ex:
            return ''

    def get_last_login(self):
        return self.send('login')

    def get_file_list(self):
        files = self.send('list')

        if len(files) > 0:
            return files.split(',')
        else:
            return ['no files available']

    def send(self, data):
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.connect((self.host, self.port))
            sock.send('{0}{1}'.format(data, self.msg_end).encode('UTF-8'))
            response = self.recv_until(sock, self.msg_end)
            sock.close()
            return response

        except Exception as ex:
            return ''

    def recv_until(self, sock, end):
        try:
            recv = sock.recv(self.buff_size).decode('utf-8')
            while recv.find(end) == -1:
                recv += sock.recv(self.buff_size).decode('utf-8')
            return recv[:recv.find(end)]

        except Exception as ex:
            return ''

    def cls(self):
        os.system('cls' if os.name == 'nt' else 'clear')


if __name__ == '__main__':
    secure_client = SecureClient()

简单浏览一下客户端的逻辑,客户端的功能有:

  • 列出服务器上的文件
  • 得到AES-ECB 加密的文件内容(但是不知道密钥)
  • 如果我们能提供正确的OTP,那么可以登录为Admin并且读出flag

如果我们进一步阅读服务器的代码,我们可以看到文件列表是硬编码的,并不是很有用。其次我们可以看到发送来的并不完全是加密的文件,而是:

content = self.read_file(file)
response = '{0}: {1}'.format(file, content)
encrypted_response = self.encrypt(response)

在文件内容外,起那面会加上我们提供的文件名!文件名会以一种奇怪的方式sanitized:

def sanitize(self, file):
    try:
        if file.find('\x00') == -1:
            file_name = file
        else:
            file_name = file[:file.find('\x00')]

        file_path = os.path.realpath('files/{0}'.format(file_name))

        if file_path.startswith(os.getcwd()):
            return file_path
        else:
            return None

可以看到只会去第一个null字节之前的内容作为文件名。
我们可以继续看看服务器上有哪些文件:

self.log_path = '../top_secret/server.log'
self.real_flag = '../top_secret/real.flag'
self.aes_key = '../top_secret/aes.key'
self.totp_key = 'totp.secret'

可以看到flag,aes_key 和log 都是不可达的,totp.secret文件是我们可以通过服务器请求到的。
现在让我们来看一下admin指令。他会校验OTP,如果正确就会发来 flag。OTP生成算法如下:

def totp(self, secret):
    counter = pack('>Q', int(time()) // 30)
    totp_hmac = hmac.new(secret.encode('UTF-8'), counter, sha1).digest()
    offset = ord(totp_hmac[19]) & 15
    totp_pin = str((unpack('>I', totp_hmac[offset:offset + 4])[0] & 0x7fffffff) % 1000000)
    return totp_pin.zfill(6)

该算法是基于时间的,我们可以通过get time 指令来获得服务器上的时间。因此唯一未知的值就是secret,该值是从totp.secret文件中读取的。如果我们能获得该文件的内容就可以计算出正确的OTP,从而以admin身份获得flag。
在之前我们提到我们获得的不仅是文件的内容,还有文件名,而且我们可以在文件名之后加任意的nullbyte,仍能够读到正确的文件。
我们可以用上述的性质来恢复加密的内容!这是因为我们可以在可以控制前缀的情况下解密任何AES-ECB密文的后缀。思路如下:

  • 将第一个块随意的填充,只留最后一个字节为可变的
  • 穷举256种情况,保持相同的前缀,只有最后一个字节不同,并加密,得到对应的256种密文
  • 对于我们想要解密的后缀suffix,让其第一字节为第一个块的最后一个字节,第一个块与第2步保持相同的前缀,并加密
  • 把第三个块加密的结果同第二步的256种情况进行比较,即可会的后缀的第一个字节
  • 不停重复上述过程,每求出一个字节,就在第一步种左移一个字节,让待求的字节占据第一个块的最后一个字节,这样每穷举256次即可求出一个字节。我们也可以扩展这个方法来求超出一个块长度的后缀。
    代码如下:
def brute_ecb_suffix(encrypt_function, block_size=16, expected_suffix_len=32, pad_char='A'):
    suffix = ""
    recovery_block = expected_suffix_len / block_size - 1
    for i in range(expected_suffix_len - len(suffix) - 1, -1, -1):
        data = pad_char * i
        correct = chunk(encrypt_function(data), block_size)[recovery_block]
        for character in range(256):
            c = chr(character)
            test = data + suffix + c
            try:
                encrypted = chunk(encrypt_function(test), block_size)[recovery_block]
                if correct == encrypted:
                    suffix += c
                    print('FOUND', expected_suffix_len - i, c)
                    break
            except:
                pass
    return suffix

对于这道题而言,应该结合如下加密函数:

def encrypt(pad):
    return send("file ../totp.secret\0\0" + pad).decode("base64")[16:]

这样就能获得totp.secret,并计算出OTP,使用admin指令

def main():
    # secret = brute_ecb_suffix(encrypt, 16, 64, '\0')[2:]
    secret = '0b25610980900cffe65bfa11c41512e28b0c96881a939a2d'
    result = send('login')
    time = int(result)
    print(send('admin ' + totp(secret, time)))
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • 本文主要介绍移动端的加解密算法的分类、其优缺点特性及应用,帮助读者由浅入深地了解和选择加解密算法。文中会包含算法的...
    苹果粉阅读 11,302评论 5 29
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,099评论 18 139
  • 这篇文章主要讲述在Mobile BI(移动商务智能)开发过程中,在网络通信、数据存储、登录验证这几个方面涉及的加密...
    雨_树阅读 2,235评论 0 6
  • 目录一、对称加密 1、对称加密是什么 2、对称加密的优点 3、对称加密的问题 4、对称加密的应用场景 5、对称加密...
    意一ineyee阅读 61,036评论 8 110
  • 水上萍阅读 121评论 0 6