如何安全快速地部署多道 ctf pwn 比赛题目

CTF相关 2019-11-10

本文作者:giantbranch(信安之路作者团队成员)

一开始接触 pwn 的时候,我们要么本地调试,要么自己用 socat 将程序启动起来远程调试

最近去搞 pwn 培训,发现将 pwn 题一个一个部署起来还是比较繁琐,除了权限还要考虑其他东西

后来一顿搜索,看看有无别人的解决方案,发现一个 xinted + docker 的方案:

https://github.com/Eadom/ctf_xinetd

但是对于这个我发现了一些缺点:

1、需要自己配置 flag

2、需要自己修改 ctf.xinetd 文件

3、没有 docker-compose.yml 方便我们去启动

4、一次只能部署一个题目(我想一键将 5 道题甚至是 10 道题同时部署在一个 docker 容器中)

5、安全性基于 chroot,而且只给了 ls,cat 和 sh 三个程序,已经很安全了,但是 sh 还是存在 fork 炸弹的可能

于是我根据自己需要,写了一个项目:

https://github.com/giantbranch/pwn_deploy_chroot

pwn_deploy_chroot 特点

1、一次可以部署多个题目到一个 docker 容器中

2、自动生成 flag,并备份到当前目录

3、也是基于 xinted + docker + chroot

4、利用 python 脚本根据 pwn 的文件名自动化地生成 3 个文件:pwn.xinetd,Dockerfile 和 docker-compose.yml

5、在 /bin 目录,利用自己编写的静态编译的 catflag 程序作为 /bin/sh,这样的话,system("/bin/sh") 实际执行的只是读取 flag 文件的内容,完全不给搅屎棍任何操作的余地

6、默认从 10000 端口监听,多一个程序就 +1,起始的监听端口可以在 config.py 配置,或者生成 pwn.xinetd 和 docker-compose.yml 后自己修改这两个文件

环境配置

安装 docker

curl -s https://get.docker.com/ | sh

安装 docker compose 和 git

apt install docker-compose git

下载

git clone https://github.com/giantbranch/pwn_deploy_chroot.git

使用

只需要 3 步:

1、将所有 pwn 题目放入 bin 目录(注意名字不带特殊字符,因为会将文件名作为 linux 用户名)

2、python initialize.py

3、docker-compose up --build -d

下面给下详细操作:

1、将你要部署的 pwn 题目放到 bin 目录

我的项目已经将一个程序 copy 了 3 分作为示例,注意文件名不要含有特殊字符,文件名建议使用字母,下划线,横杆和数字,当然全字母的当然最好了

root@instance-1:~/pwn_deploy_chroot# ls bin/
pwn1 pwn1_copy1 pwn1_copy2

2、运行 initialize.py

运行脚本后会输出每个 pwn 的监听端口,

root@instance-1:~/pwn_deploy_chroot# python initialize.py

pwn1's port: 10000
pwn1_copy1's port: 10001
pwn1_copy2's port: 10002

文件与端口信息,还有随机生成的 flag 默认备份到 flags.txt

root@instance-1:~/pwn_deploy_chroot# cat flags.txt 
pwn1: flag{93aa6da5-db45-46fa-a2e1-af2be6698692}
pwn1_copy1: flag{f9966c51-52e4-4212-ac44-97bf16620b41}
pwn1_copy2: flag{b17949ce-e3fa-4ca7-9fcc-44b8dc997cb3}

pwn1's port: 10000
pwn1_copy1's port: 10001
pwn1_copy2's port: 10002

3、启动环境

请使用 root 用户执行命令

docker-compose up --build-d

不出意外,题目就启动起来了

root@instance-1:~/pwn_deploy_chroot# netstat -antp | grep docker
tcp6       0     0:::10002               :::*                   LISTEN      19828/docker-proxy
tcp6       0     0:::10000               :::*                   LISTEN      19887/docker-proxy
tcp6       0     0:::10001               :::*                   LISTEN      19873/docker-proxy

我们测试一下 pwn1,看看效果

img

可以看到,虽然执行的是 system("/bin/sh"),但是实际功能只是输出 flag,这样就非常安全了

原理介绍

简单概括

利用 initialize.py 脚本根据 pwn 的文件名自动化地生成 3 个文件:pwn.xinetd,Dockerfile 和 docker-compose.yml,之后便可以 docker 启动了

详细说明

config.py

首先在 config.py 中定义了一些常量,路径,pwn 题起始监听的端口,XINETD 配置文件模板,Dockerfile 模板,还有 docker-compose.yml 模板

img

重点介绍 initialize.py

步骤1:获取 bin 目录的文件列表

defgetFileList():
   filelist= []
   forfilenameinos.listdir(PWN_BIN_PATH):
       filelist.append(filename)
   filelist.sort()
   returnfilelist

步骤2:通过 uuid 库随机生成 flag 并备份到 flags.txt 文件,方便我们查看

defgenerateFlags(filelist):
   tmp= ""
   flags= []
   ifos.path.exists(FLAG_BAK_FILENAME):
       os.remove(FLAG_BAK_FILENAME)
   withopen(FLAG_BAK_FILENAME, 'a') asf:
       forfilenameinfilelist:
           tmp= "flag{"+str(uuid.uuid4()) +"}"
           f.write(filename+": "+tmp+"\n")
           flags.append(tmp)
   returnflags

步骤3:将每个 pwn 题所对应的监听端口也写到 flags.txt 文件中

defgenerateBinPort(filelist):
   port= PORT_LISTEN_START_FROM
   tmp= "\n"
   forfilenameinfilelist:
       tmp+= filename +"'s port: "+str(port) +"\n"
       port= port+1
   printtmp
   withopen(FLAG_BAK_FILENAME, 'a') asf:
       f.write(tmp)

步骤4:根据 pwn 题的文件名,端口,uid 去格式化 XINETD 配置文件模板

defgenerateXinetd(filelist):
   port= PORT_LISTEN_START_FROM
   conf= ""
   uid= 1000
   forfilenameinfilelist:
       conf+= XINETD%(port, str(uid) +":"+str(uid), filename, filename)
       port= port+1
       uid= uid+1
   withopen(XINETD_CONF_FILENAME, 'w') asf:
           f.write(conf)

步骤5:生成 Dockerfile,具体是先以 pwn 题的文件名去创建用户,并将对应的 pwn 程序复制到其家目录,将自己编写并静态编译的 catflag 程序复制到其家目录的 /bin/sh, 之后就是一些权限的设置,还有 lib 目录的拷贝

defgenerateDockerfile(filelist, flags):
   conf= ""
   # useradd and put flag
   runcmd= "RUN "
   
   forfilenameinfilelist:
       runcmd+= "useradd -m "+filename+" && "
  
   forxinxrange(0, len(filelist)):
       ifx== len(filelist) -1:
           runcmd+= "echo '"+flags[x] +"' > /home/"+filelist[x] +"/flag.txt"
       else:
           runcmd+= "echo '"+flags[x] +"' > /home/"+filelist[x] +"/flag.txt"+" && "
   # print runcmd 

   # copy bin
   copybin= ""
   forfilenameinfilelist:
       copybin+= "COPY "+PWN_BIN_PATH+"/"+filename +" /home/"+filename+"/"+filename+"\n"
       copybin+= "COPY ./catflag"+" /home/"+filename+"/bin/sh\n"   
   # print copybin

   # chown & chmod
   chown_chmod= "RUN "
   forxinxrange(0, len(filelist)):
       chown_chmod+= "chown -R root:"+filelist[x] +" /home/"+filelist[x] +" && "
       chown_chmod+= "chmod -R 750 /home/"+filelist[x] +" && "
       ifx== len(filelist) -1:
           chown_chmod+= "chmod 740 /home/"+filelist[x] +"/flag.txt"
       else:
           chown_chmod+= "chmod 740 /home/"+filelist[x] +"/flag.txt"+" && "
   # print chown_chmod

   # copy lib,/bin 
   dev= '''mkdir /home/%s/dev && mknod /home/%s/dev/null c 1 3 && mknod /home/%s/dev/zero c 1 5 && mknod /home/%s/dev/random c 1 8 && mknod /home/%s/dev/urandom c 1 9 && chmod 666 /home/%s/dev/* '''
   copy_lib_bin_dev= "RUN "
   forxinxrange(0, len(filelist)):
       copy_lib_bin_dev+= "cp -R /lib* /home/"+filelist[x]  +" && "       
       copy_lib_bin_dev+= dev%(filelist[x], filelist[x], filelist[x], filelist[x], filelist[x], filelist[x])
       ifx== len(filelist) -1:
           pass               
       else:    
           copy_lib_bin_dev+= " && "

   conf= DOCKERFILE%(runcmd, copybin, chown_chmod, copy_lib_bin_dev)

   withopen("Dockerfile", 'w') asf:
       f.write(conf)

步骤6:生成 docker-compose.yml,根据之前的端口信息,格式化一下 docker-compose.yml 的模板就行了

defgenerateDockerCompose(length):
   conf= ""
   ports= ""
   port= PORT_LISTEN_START_FROM
   forxinxrange(0,length):
       ports+= "- "+str(port) +":"+str(port) +"\n   "
       port= port+1

   conf= DOCKERCOMPOSE%ports
   # print conf
   withopen("docker-compose.yml", 'w') asf:
       f.write(conf)
catflag 程序

这个程序非常简单,就只是读取 flag.txt 文件并输出

#include <stdio.h>

intmain()
{
   FILE*fp= NULL;
   charbuff[255];
   fp= fopen("/flag.txt", "r");
   fgets(buff, 255, (FILE*)fp);
   printf("%s\n", buff);
   fclose(fp);
}
XINETD配置文件说明

下面是 initialize.py 所生成 xinetd 配置

servicectf
{
   disable= no
   socket_type= stream
   protocol  = tcp
   wait      = no
   user      = root
   type      = UNLISTED
   port      = 10000
   bind      = 0.0.0.0
   server    = /usr/sbin/chroot  
   server_args= --userspec=1000:1000/home/pwn1./pwn1
   # safety options
   per_source= 10# the maximum instances of this service per source IP address
   rlimit_cpu= 20# the maximum number of CPU seconds that the service may use
   rlimit_as= 100M# the Address Space resource limit for the service
   #access_times = 2:00-9:00 12:00-24:00
}

功能就是以 root 用户启动 /usr/sbin/chroot(这个程序就是改变根目录),并且执行是以 uid 为 1000,gid 为 1000 的权限启动,改变后的根目录为 /home/pwn1, 启动的程序为 ./pwn1,由于根目录被改变了,启动的就是根目录的 pwn1,即这个程序—— /home/pwn1/pwn1

当然你还可以配置几个选项去限制一些东西:

per_source 限制一个 ip 最大的连接数

rlimit_cpu 当前服务使用的最大 CPU 时间

rlimit_as 限制使用的最大内存

不足的地方

这个作为线上赛的 pwn 题部署暂时应该没什么其他问题了,还可以搅屎的可以联系我看看怎样还可以搅屎

但是作为线下赛的话,没有考虑限制防御队伍使用通防

此外

最后再给一下项目地址,欢迎在各种 CTF 线上赛使用,顺手 star 一下:

https://github.com/giantbranch/pwn_deploy_chroot

我将项目 bin 目录下的示例程序 pwn1 部署了起来,是最简单的栈溢出,大家可以下载下来,尝试打下我的服务器,看看这种部署方式还有没有其他安全问题需要解决,还可以怎样搅屎

远程服务器地址及端口:(两个星期之后会下线,赶紧打吧)

nc pwntest.giantbranch.cn 10000

假如你没有 pwn 的做题环境,可以考虑我的 CTF PWN 做题环境一键搭建脚本(理论上适用于 debian 系的 linux x64 系统)

https://github.com/giantbranch/pwn-env-init

主要搞了以下几个功能

1、为 64 位系统提供 32 位运行环境支撑

2、下载了 libc6 的源码,方便源码调试,可看这:

https://blog.csdn.net/u012763794/article/details/78457973

3、给 gdb 装上 pwndbg 和 peda 插件

4、安装 pwntools

感谢

https://github.com/Eadom/ctf_xinetd


本文由 信安之路 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

楼主残忍的关闭了评论