LANCTF(In BUAA) 随笔

LANCTF

LANCTF 是 Lancet 举办的校内 CTF 比赛,出题人包含了绝大多数 Lancet 核心成员。由于面向校内,难度以中等偏下为主。

本人咸甚,在比赛中仅承担了 LANCTFd(based on CTFd) 的二次开发与维护、部分(简单) Web 题目出题人,在这里记录一下自出题以来的想法以及比赛期间的维护、交流等内容。

Web

UnderGroundCity1

题目设置

题目原型:TWCTF2018 shrine

出原题过于简单(难),为了将难度稳定设置在简单,且符合比赛主题,修改了一下界面(偷了 CTFd 的页面,简化了过滤规则。

flag 设置在 config 中,过滤规则如下

@app.errorhandler(404)
def page_not_found(e):
    uurl=request.url

    if ">" in request.url or "&" in request.url or "-" in request.url or "%26" in request.url or "%3e" in request.url.lower():
        return "FORBIDEN"
    if "os" in request.url or  "self" in request.url or "system" in request.url:
        return "I know what you did! But flag you want is in config."
    if "'current_app" in request.url:
        uurl= "LANCTF{ .... <br> 抄current_app也要懂ssti才对嘛,你的current_app好像被过滤了"
    
    template = '''
    {{% set config='flag is in config, but var config was clear'%}}
    ......
    {{url}}
    '''.format(url=uurl)
    return render_template_string(template), 404

第一次判断避免反弹 shell,对&的过滤有些多余,但不影响;第二次判断过滤关键字,是 SSTI 题目常见简单过滤;第三次额外设置防止有直接复制 TWCTF 的 payload。

和简单 SSTI 没什么不同,关于 SSTI 可参考 从SSTI到沙箱逃逸-jinja2,在这里想记一下维护中遇到的小问题。

启动管理

题目直接使用 python 启动,多次莫名退出,于是第一天更换为 gunicorn 启动,但在设置参数时埋下了一个坑,su test -c 'gunicorn -w 4 -b 0.0.0.0:8000 --daemon web:app' ,题目使用四进程启动,导致四个初始化的子进程内置类顺序不同,表现在使用 __subclasses__()[59] 时无法固定子类下标,出现这个错误也是因为出题经验太少。

(看日志发现被打了一堆 request.environ['werkzeug.server.shutdown']() ,直接起 flask 服务默认使用的便是 werkzeug)

发现问题后又切换回 python 启动,却没有思考原因,直接换为 supervisor 启动。在 python3 环境下需要自行配置 conf 文件,于是直接使用 apt install supervisor,添加 test 用户,copy 如下文件到 /etc/supervisor/conf.d 文件夹,之后开启服务即可。

[program:ssti]
directory = /var/www/html ; 程序的启动目录
command = python3 /var/www/html/web.py  ; 启动命令
autostart = true     ; 在 supervisord 启动的时候也自动启动
startsecs = 5        ; 启动 5 秒后没有异常退出,就当作已经正常启动了
autorestart = true   ; 程序异常退出后自动重启
startretries = 3     ; 启动失败自动重试次数,默认是 3
user = test          ; 用哪个用户启动
redirect_stderr = true  ; 把 stderr 重定向到 stdout,默认 false
stdout_logfile_maxbytes = 20MB  ; stdout 日志文件大小,默认 50MB
stdout_logfile_backups = 20     ; stdout 日志文件备份数
; stdout 日志文件,需要注意当指定目录不存在时无法正常启动
stdout_logfile = /var/log/ssti_stdout.log

第二天才想到 __subclasses__() 的变化与进程数有关,只需要设置成子进程为1即可。(菜

UnderGroundCity2

题目原型:HITB2018 某题

题目设计很明确 哈希长度扩展攻击以及 python 的代码审计,未进行大的变动,稍稍降低了难度。使用 gunicorn 启动。

可参考 hash长度扩展攻击研究

BlackBox AND Easy PHP

题目原型:经典上传题

均对内容进行了过滤,分别使用 preg_replace 移除了 <??,故需使用 <script language="php">system($_GET[0]);</script> 方式进行绕过,对于过滤前者可双写 <<??

需要注意的是,在 php7 中 script 标签不再支持。

此外, EasyPHP 设置了一处 json 弱类型,来自 XMAN2018。

	if(!isset($_COOKIE['authe'])){
		//secret_is_'hash.??????'
		$autharr=array(
			'role'=>'guest',
			'passnum'=>'????????'
			);
		$auth= json_encode($autharr);
		ob_start();
		setcookie('authe', $auth);
		ob_end_clean();
		$_SESSION['isguest']=true;
	  }else{
		$temp=$_COOKIE['authe'];
		$data=json_decode($temp);
		$num=$data->passnum;
		if(json_last_error() != JSON_ERROR_NONE){
			echo "json error";
			exit();
		}
		if($num!=="????????"){
			for ($i=0; $i < 8; $i++) { 
				//secret num is random generated that you can't guess
				if(!($num[$i]==$secretnum[$i]))
				{
					echo "random secret num error";
					exit();
				}
			}
			if($data->role==='admin'){
				$_SESSION['isguest']=false;
			}
		}

FoodWithPHP

题目原型:n1ctf

大量改动,简化,只保留了文件包含,从文件包含读源码,最后包含 session,从而 getshell。

Re

Father and son

题目逻辑:main 启动,fork 自身,子进程生成 core,设置运行权限,execl 调用 core,父进程 ptrace 修改 core 其中的加密密码,之后子进程输出 flag(前几位)。

__int64 sub_400BAE()
{
  __pid_t v0; // eax
  unsigned int v2; // [rsp+Ch] [rbp-154h]
  char v3; // [rsp+10h] [rbp-150h]
  char v4; // [rsp+F0h] [rbp-70h]
  __int16 v5; // [rsp+148h] [rbp-18h]
  unsigned __int64 v6; // [rsp+158h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  memset(&v4, 0, 0x58uLL);
  v5 = 0;
  v0 = fork();
  v2 = v0;
  if ( v0 == -1 )
    exit(1);
  if ( v0 )
  {
    wait(0LL);
    if ( ptrace(PTRACE_GETREGS, v2, 0LL, &v3) < 0 )
    {
      perror("ptrace(GETREGS):");
      exit(1);
    }
    ptrace(PTRACE_POKEDATA, v2, &unk_6020AC, 7LL);
    ptrace(PTRACE_CONT, v2, 0LL, 0LL);
    wait(0LL);
  }
  else
  {
    ptrace(0, 0LL, 0LL, 0LL);
    execl("./core", "./core", 0LL);
  }
  return 0LL;
}

正如上面所看到,ptrace 的使用流程一般是这样的:父进程 fork() 出子进程,子进程中执行我们所想要 trace 的程序,在子进程调用 execl() 之前,子进程需要先调用一次 ptrace,以 PTRACE_TRACEME(0) 为参数。这个调用是为了告诉内核,当前进程已经正在被 traced,当子进程执行 execve() 之后,子进程会进入暂停状态,把控制权转给它的父进程(SIG_CHLD信号), 而父进程在fork()之后,就调用 wait() 等子进程停下来,当 wait() 返回后,父进程就可以去查看子进程的寄存器或者对子进程做其它的事情了。

当系统调用发生时,首先检查了能否获取寄存器 REGS,若可以,便修改内存中的值,当做完这些事情之后,通过调用 ptrace(PTRACE_CONT) ,可以让子进程重新恢复运行。此时 core 之中的内存已被修改。

signed __int64 sub_400914()
{
  signed __int64 result; // rax
  signed int i; // [rsp+4h] [rbp-Ch]
  unsigned int *v2; // [rsp+8h] [rbp-8h]

  v2 = (unsigned int *)&unk_6020C0;
  for ( i = 0; i <= 3; ++i )
  {
    sub_400834(v2, &unk_6020A0);
    result = 8LL;
    v2 += 2;
  }
  return result;
}

__int64 __fastcall sub_400834(unsigned int *a1, int *a2_6020A0)
{
  __int64 result; // rax
  unsigned int v3; // [rsp+1Ch] [rbp-24h]
  unsigned int v4; // [rsp+20h] [rbp-20h]
  unsigned int i; // [rsp+24h] [rbp-1Ch]
  signed int v6; // [rsp+28h] [rbp-18h]

  v3 = *a1;
  v4 = a1[1];
  v6 = -478700656;
  for ( i = 0; i <= 0xF; ++i )
  {
    v4 -= (v3 + v6) ^ (16 * v3 + a2_6020A0[2]) ^ ((v3 >> 5) + a2_6020A0[3]);
    v3 -= (v4 + v6) ^ (16 * v4 + *a2_6020A0) ^ ((v4 >> 5) + a2_6020A0[1]);
    v6 += 1640531527;
  }
  *a1 = v3;
  result = v4;
  a1[1] = v4;
  return result;
}

可以看到修改了 tea 加密算法的密码。

出题人太秀了(

LanCTFd

LanCTFd 在 CTFd 的基础上为比赛需要测试并修改了一些功能,为了更好的保持性能,使用了 CTFd 提供的所有组件 CTFd+Redis+Mysql。其中的一些小修改已写在 readme 中,bug 已经提交修改啦。

但是在比赛中也发现了一些问题:服务器响应时间过长,在阿里云平台查看监控数据发现平均负载1,双核情况下没有问题,TCP连接数1000,而ESTABLISHED状态的也只有个位数,其他的均为未连接状态,单独重启 CTFd 后,连接数降为100左右,后又持续上升。对于这个问题一直没有想明白,猜测可能是gunicorn 的长连接导致的吧,但是为什么会导致服务器响应时间过长呢?(服务器 2C4G Intel Xeon 2.5 GHz)

在赛后开放的过程中发现了一次宕机的情况,这次连接数达到了1200个,查看 docker ctfd 的连接数,约1200个,基本符合。有两种可能性,一是阿里云的机器限制了 tcp 连接的 ESTABLISHED 状态的上限数量;二是 gunicorn 启动默认配置工作状态为 gevent,次数 worker_connections 默认值为 1000,当进程状态下无法响应更多的连接。从阿里云这边来看显然为后者的问题,但是还没有做测试来证实,而且官方的默认配置也应该不会有这样的问题。不过还是后者的可能性更大。在之后的配置中可以考虑--worker-connections=2000,并且增加到四个进程-w 4

CTFd

整体 CTFd 服务使用 docker-compose 部署,使用三个容器组成内网服务,其中 CTFd 由 gunicorn 管理,整体环境在 Python2.7 下。对 CTFd 的主要更改如下:

  1. 添加谷歌验证码,支持大陆环境(via ctfd-recaptcha-plugin)
  2. 修改 SMTP 服务器发信内容,减少被服务器拒绝概率;修复 SMTP 发信返回值未判断的 bug(写本文时官方已在几小时前修复)
  3. 控制未验证账号重发邮件次数,在 session 中硬编码实现
  4. 动态积分插件,计分公式适应性修改,适配20人解出时达到最低值,使用自定义公式
  5. 修复动态积分插件中,隐藏用户提交 flag 后造成分数变化的 bug
  6. 修复由隐藏用户导致的,普通用户在 profile 中排名与 scoreboard 排名不一致的 bug

目前 bug 都已经提上去了,还是我 PR 的代码太烂了作者要自己写(

部署文件

为了折腾整套服务。并且保证一定的稳定性,不得不了解一下每个参数的作用。

Docker file

这里直接贴略微更改后的镜像文件

FROM python:2.7-alpine
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories
# 运行 bash 命令,替换更新源
RUN apk add --update-cache bash
RUN apk update && \
    apk add python python-dev linux-headers libffi-dev gcc make musl-dev py-pip mysql-client git openssl-dev libxml2-dev libxslt-dev
# alpine 系统使用 apk 命令,添加部分库,支持验证码插件
WORKDIR /opt/CTFd
#下面的 ADD, COPY, CMD, ENTRYPOINT, RUN 命令的相对目录
RUN mkdir -p /opt/CTFd
COPY requirements.txt .

RUN pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
# 同理,换源
COPY . /opt/CTFd

VOLUME ["/opt/CTFd"]
# VOLUME 指令可以在镜像中创建挂载点,这样只要通过该镜像创建的容器都有了挂载点。通过 VOLUME 指令创建的挂载点,无法指定主机上对应的目录,是自动生成的。需通过 docker inspect 查看。
RUN for d in CTFd/plugins/*; do \
      if [ -f "$d/requirements.txt" ]; then \
        pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r $d/requirements.txt; \
      fi; \
    done;
    
RUN chmod +x /opt/CTFd/docker-entrypoint.sh
EXPOSE 8000
ENTRYPOINT ["/opt/CTFd/docker-entrypoint.sh"]
# 容器入口点脚本

docker-compose file

官方默认配置如下:

version: '2'

services:
  ctfd:
    build: .
    # 镜像在当前目录创建
    restart: always
    # 自动重启 no / always / on-failure / unless-stopped
    ports:
      - "8000:8000"
    environment:
      - UPLOAD_FOLDER=/var/uploads
      - DATABASE_URL=mysql+pymysql://root:ctfd@db/ctfd
      - REDIS_URL=redis://cache:6379
      - WORKERS=1
      - LOG_FOLDER=/var/log/CTFd
      - ACCESS_LOG=-
      - ERROR_LOG=-
    volumes:
      - .data/CTFd/logs:/var/log/CTFd
      - .data/CTFd/uploads:/var/uploads
      - .:/opt/CTFd:ro
    # 挂载目录
    depends_on:
      - db
    # 依赖
    networks:
    # 网络,可见有两个子网
        default:
        internal:

  db:
    image: mariadb:10.4
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=ctfd
      - MYSQL_USER=ctfd
      - MYSQL_PASSWORD=ctfd
    volumes:
    # 挂载以保存数据
      - .data/mysql:/var/lib/mysql
    networks:
        internal:
    # This command is required to set important mariadb defaults
    command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --wait_timeout=28800, --log-warnings=0]

  cache:
    image: redis:4
    restart: always
    volumes:
    - .data/redis:/data
    networks:
        internal:

networks:
# 网络定义,同网络下可访问,使用 service 名称即可,docker 会完成名称解析
    default:
    internal:
        internal: true
        # 真内网,不可访问互联网

Compose file version 3 reference

Updated At: Author:xmsec
Chief Water dispenser Manager of Lancet, delivering but striving.
Github
comments powered by Disqus