Introduction

寒假的时候学了一下Flask,开发了自己现在的博客,本来寒假的时候就已经要准备开发一个CTF平台给实验班考核的时候用的,但是一直没有规划好架构,所以一直延期了。
一直是知道CTFD这个平台的,但是一直没有仔细研究过,直到最近研读了一下源码和文档,发现他是真的香!所以就有了这篇文章的诞生,记录一下魔改的过程和遇到的坑点。

Dir Structure

CTFd
├── CTFd
│   ├── admin # 后台
│   ├── api # API
│   ├── cache # 缓存设置
│   ├── events # 全局event事件(弹窗)设置
│   ├── logs # 日志文件夹
│   ├── models # 数据库结构
│   ├── plugins # 插件(二次开发)
│   │   ├── challenges
│   │   │   └── assets
│   │   └── dynamic_challenges
│   │       └── assets
│   ├── schemas # 封装的Class
│   ├── themes
│   │   ├── admin # 后台前端主题
│   │   │   ├── static
│   │   │   └── templates
│   │   └── core # 前台前端主题
│   │       ├── static
│   │       └── templates
│   ├── uploads # 上传的文件夹
│   └── utils # 封装的各模块函数
│       ├── config
│       ├── countries
│       ├── crypto
│       ├── dates
│       ├── decorators # 这里访问速率设置
│       ├── email
│       ├── encoding
│       ├── events
│       ├── exports
│       ├── helpers
│       ├── initialization # 这里有CSRF设置
│       ├── logging
│       ├── migrations
│       ├── modes
│       ├── notifications
│       ├── plugins
│       ├── scores
│       ├── security
│       ├── sessions
│       ├── updates
│       ├── uploads
│       ├── user
│       └── validators
├── docs
├── migrations
│   └── versions
├── scripts
└── tests # 功能模块测试
    ├── admin
    ├── api
    ├── challenges
    ├── oauth
    ├── teams
    ├── users
    └── utils

其实基本上能魔改的地方就前端和CTFD可供二次开发的Plugins,只需要大开脑洞,想好要加什么功能,就可以直接开干,因为是真的不难

Front-end Transformation

前端其实没啥可改的,就是换下配色 / 换下布局,其他地方都是jinja2动态输出,照搬原样就可以了。
CTFD后台其实支持增加独立Page,但是如果不需要大改的话,直接用那个也能满足一些需求。
但是作为一个“爱折腾的”水瓶儿,肯定不满足原版CTFD的美工,就自己魔改了一个新的。

Notification

加上Timeline会稍微好看一些
WX20190705-193035@2x_shrink

Matrix

自制记分板,这个在下一Part的Plugins里面会仔细讲
WX20190705-193735@2x_shrink

Challenges

这里的配色一直没调好,但是还凑合
WX20190705-193826@2x_shrink

Plugins Transformation

看下面这一部分之前,建议先看看官方WIKI里关于Plugins的介绍

Dynamic Instance Challenge

这里分享一下2018级实验班考核时用到的动态下发题目容器的Plugins
其实就是自己二次开发一个新的Challenge Type,官方demo中也包含了一个编程(Programming)的新Challenge Type,感兴趣的可以也去研究一下。
因为动态下发题目的Challenge Type跟平时的static类型没啥区别,只需要自己自定义一下

  • 新Challenge额外的数据库字段(如部署方式、题目目录等)
  • attempt()判断Flag对错 / 作弊与否

Dynamic Instance Distribution

上面的只是新的Challenge Type,虽然我们可以也可以把动态下发容器的代码放到上面一起,但是感觉分开不同的模块对后续的优化更便利,所以重新创建一个Plugin写我们动态下发容器。
这里新建Plugin的模版就可以不抄上面的了,只需要有一个load()函数用于app启动时导入插件即可,剩下的就是一般的Python函数编写。
容器的下发有两种可选方案:

  1. docker
  2. docker-compose

两种方案其实差不多,第一种适合类AWD那种直接挂载源码就能跑那种,第二种兼容各种出题环境,给出题人更大的发挥空间。
但是都有一些Bug就是:

  1. 都需要调用Docker API,而且宿主机与API主机的Docker Volume目录要相同,不然无法挂载目录
  2. 出内网题时,容易有BUG(在同一网段,会打到隔壁老王的机子;不在同一网段,没有足够的内网IP分配导致不能支持多人同时做题)

最后我采用如下方案
ctfd_shrink

Matrix Scoreboard

这个记分板的想法来源于一开始看到官方github的plugin库里面有大牛开发的记分板,但是发现他是老版本的,所以还是要做一个适配v2.*版本的才能用,于是又自己魔改了一波,加了一二三血的Flag和适配一二三血的总分算法,改完还是很满意的,但是分页一直没弄,因为要加分页感觉后端代码改动有点大,所以就先不折腾了。先用着,等到后面有时间再改善一下。

Tricks

Nginx Reverse Proxy

Notification

如果需要反代CTFD的话,需要注意:Notification那个events需要额外配置这个,因为他是服务端主动推送的,所以连接需要Keep-Alive保持连接,否则管理员发全员弹窗的时候会无法成功发送。

location /events {
        proxy_pass  http://d0g3-ctfd:4000/events;
        proxy_set_header Connection '';
        proxy_http_version 1.1;
        chunked_transfer_encoding off;
        proxy_buffering off;
        proxy_cache off;
    }

附上我的Nginx配置给大家参考

location / {
        proxy_pass http://d0g3-ctfd:4000;

        #ssl settings
        proxy_set_header X-Forwarded-Port $server_port;

        #settings
        # 如果遇到前端https,后端http可以尝试下面注释里的设置
        # proxy_redirect     http:// https://;
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
        proxy_max_temp_file_size 0;
        proxy_hide_header X-Frame-Options;
        add_header X-Frame-Options SAMEORIGIN always;
        proxy_connect_timeout      90;
        proxy_send_timeout         90;
        proxy_read_timeout         90;
        proxy_temp_file_write_size 64k;
    }

Automatic register

自动注册用户脚本

# -*- coding: utf-8 -*-
# @Author: 0aKarmA_骅文
# @Date:   2019-06-27 14:19:25
# @Last Modified by:   0aKarmA
# @Last Modified time: 2019-06-27 23:46:49


import requests, re

def register_user(left, right):
    for i in range(left,right):
        name = "TEST" + str(i)
        email = name + "@d0g3.cn"
        url = 'https://domain/register'
        s = requests.Session()
        rq = s.get(url)
        token = "".join(re.findall(r'nonce" value="(.*)"', rq.text))
        data = {
            "name": name,
            "email": email,
            "password": "password",
            "nonce" : token
        }
        rqq = s.post(url, data)
        print("Success Register " + name + " " + str(rqq.status_code))            


if __name__ == '__main__':

    register_user(0,10)

Summary

平台和题目经过80+同学同时在线并发测试,负载远低于我自己做100+的异步并发~~简直有点不敢相信。。
除了下发不同题目容器时需要清理浏览器缓存(如果不固定端口的话,应该不会存在这个问题),其他的体验应该还是不错的。