本文作者: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
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,看看效果
可以看到,虽然执行的是 system("/bin/sh")
,但是实际功能只是输出 flag,这样就非常安全了
利用 initialize.py 脚本根据 pwn 的文件名自动化地生成 3 个文件:pwn.xinetd,Dockerfile 和 docker-compose.yml,之后便可以 docker 启动了
首先在 config.py 中定义了一些常量,路径,pwn 题起始监听的端口,XINETD 配置文件模板,Dockerfile 模板,还有 docker-compose.yml 模板
步骤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)
这个程序非常简单,就只是读取 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);
}
下面是 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