├── .gitignore ├── LICENSE ├── README.md ├── config.json ├── docker └── pwndocker │ ├── Dockerfile │ ├── docker-compose.yml │ └── xinetd ├── forward.py ├── get_pwn_data.py ├── misc.py ├── mysocket.py ├── pwn ├── note │ ├── RNote3 │ ├── startdocker.sh │ └── startpwn.sh ├── pwn1 │ ├── printf │ ├── startdocker.sh │ └── startpwn.sh └── sh │ ├── startdocker.sh │ └── startpwn.sh ├── pwnserver.py └── watcher.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | test.py -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 plusls 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pwn-server 2 | 3 | 这是一个自动化部署pwn题的工具 4 | 5 | 有如下特性: 6 | 7 | 1.自动化部署pwn 8 | 9 | 2.所有的pwn题都在不同的容器中,pwn-server将会自动化的创建容器 10 | 11 | 3.只占用一个端口,会根据不同的token将连接分发到不同的容器 12 | 13 | 4.支持一题多flag 14 | 15 | 5.自动记录pwn题流量 16 | 17 | 6.自动标记出读取了flag的流量 18 | 19 | 7.防fork bomb 20 | 21 | 8.支持多镜像,既不同容器可以指定不同的docker镜像 22 | 23 | ### 依赖 24 | 25 | fanotify 26 | 27 | ``` 28 | pip3 install git+https://github.com/google/python-fanotify.git --user 29 | ``` 30 | 31 | 32 | 33 | ### 使用 34 | 35 | 第一次使用时需要build基础容器 36 | 37 | build基础容器: 38 | 39 | ``` 40 | docker build --tag cnss/pwn ./docker/pwndocker 41 | ``` 42 | 43 | 44 | 45 | 启动pwn-server 46 | 47 | ```python 48 | sudo python3 pwnserver.py 49 | ``` 50 | 51 | 目前并没有封装为服务,无守护程序 52 | 53 | 注:关闭容器后容器将会被自动删除 54 | 55 | 由于使用了fanotify,所以必须要有root权限 56 | 57 | **请务必保证pwn目录下的文件是可执行的!!!** 58 | 59 | 60 | 61 | ### 自动化部署pwn 62 | 63 | 与其它脚本不同,用pwn-server部署pwn题只需要将pwn的二进制文件丢入pwn文件夹,pwn-server会自动的识别pwn题 64 | 65 | #### pwn题目录结构 66 | 67 | 仓库中默认有2个题目在目录pwn下 68 | 69 | pwn目录结构: 70 | 71 | ```sh 72 | pwn 73 | ├── note 74 | │   ├── RNote3 75 | │   ├── startdocker.sh 76 | │   └── startpwn.sh 77 | └── pwn1 78 | ├── printf 79 | ├── startdocker.sh 80 | └── startpwn.sh 81 | ``` 82 | 83 | 其中包含了2个pwn题 分别为note和pwn1 84 | 85 | startdocker.sh默认为 86 | 87 | ```sh 88 | /usr/sbin/xinetd -dontfork 89 | ``` 90 | 91 | 这是docker运行时执行的命令 92 | 93 | startpwn.sh为xinetd会执行的命令,对于pwn1而言就是 94 | 95 | ```sh 96 | stdbuf -i 0 -o 0 -e 0 ./printf 97 | ``` 98 | 99 | #### 多flag支持 100 | 101 | 对于每一个connect,在连接上后会要求输入token,之后交由**get_pwn_data.py**处理 102 | 103 | 若是要支持多flag则可做以下设置 104 | 105 | ```python 106 | def get_pwn_data(token): 107 | '''参数为token 返回该token对应的题目名和flag以及image_tag''' 108 | if token == 'note_01': 109 | return ('note', "cnss{it_is_note_01}", "cnss/pwn") 110 | elif token == 'note_02': 111 | return ('note', 'cnss{it_is_note_02}', "cnss/pwn") 112 | 113 | if token == 'pwn1_01': 114 | return ('pwn1', "cnss{it_is_pwn1}", "cnss/pwn") 115 | 116 | return('', '', '') 117 | ``` 118 | 119 | **token=note_01** 120 | 121 | ```bash 122 | [*] Switching to interactive mode 123 | [DEBUG] Received 0x51 bytes: 124 | "*** Error in `./RNote3': munmap_chunk(): invalid pointer: 0x00007fd7f0bd7afd ***\n" 125 | *** Error in `./RNote3': munmap_chunk(): invalid pointer: 0x00007fd7f0bd7afd *** 126 | $ ls -al ../ 127 | [DEBUG] Sent 0xb bytes: 128 | 'ls -al ../\n' 129 | [DEBUG] Received 0x154 bytes: 130 | 'total 25\n' 131 | 'drwxr-xr-x 1 pwn pwn 4096 Aug 15 05:56 .\n' 132 | 'drwxr-xr-x 1 root root 4096 Aug 15 03:33 ..\n' 133 | '-rw-r--r-- 1 pwn pwn 220 Aug 31 2015 .bash_logout\n' 134 | '-rw-r--r-- 1 pwn pwn 3771 Aug 31 2015 .bashrc\n' 135 | 'drwxrwxrwx 1 root root 0 Aug 15 04:58 bin\n' 136 | '-rwxrwxrwx 1 root root 19 Aug 15 05:56 flag\n' 137 | '-rw-r--r-- 1 pwn pwn 655 May 16 2017 .profile\n' 138 | total 25 139 | drwxr-xr-x 1 pwn pwn 4096 Aug 15 05:56 . 140 | drwxr-xr-x 1 root root 4096 Aug 15 03:33 .. 141 | -rw-r--r-- 1 pwn pwn 220 Aug 31 2015 .bash_logout 142 | -rw-r--r-- 1 pwn pwn 3771 Aug 31 2015 .bashrc 143 | drwxrwxrwx 1 root root 0 Aug 15 04:58 bin 144 | -rwxrwxrwx 1 root root 19 Aug 15 05:56 flag 145 | -rw-r--r-- 1 pwn pwn 655 May 16 2017 .profile 146 | $ cat ../flag 147 | [DEBUG] Sent 0xc bytes: 148 | 'cat ../flag\n' 149 | [DEBUG] Received 0x13 bytes: 150 | 'cnss{it_is_note_01}' 151 | cnss{it_is_note_01}$ 152 | ``` 153 | 154 | **token=note_02** 155 | 156 | ```bash 157 | [*] Switching to interactive mode 158 | [DEBUG] Received 0x51 bytes: 159 | "*** Error in `./RNote3': munmap_chunk(): invalid pointer: 0x00007fad2858cafd ***\n" 160 | *** Error in `./RNote3': munmap_chunk(): invalid pointer: 0x00007fad2858cafd *** 161 | $ cat ../flag 162 | [DEBUG] Sent 0xc bytes: 163 | 'cat ../flag\n' 164 | [DEBUG] Received 0x13 bytes: 165 | 'cnss{it_is_note_02}' 166 | cnss{it_is_note_02}$ 167 | 168 | ``` 169 | 170 | docker 容器列表 171 | 172 | ```bash 173 | docker ps 174 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 175 | 335ac415ef15 7d472f349e2d "/bin/sh -c /ctf/pwn…" 4 minutes ago Up 4 minutes 1337/tcp note_02 176 | 92de3f097e93 7d472f349e2d "/bin/sh -c /ctf/pwn…" 7 minutes ago Up 7 minutes 1337/tcp note_01 177 | 178 | ``` 179 | 180 | 当然,可以自定义该函数与ctf平台进行交互,从而做到反作弊 181 | 182 | #### 多镜像支持 183 | 184 | 考虑到不同题目可能要求的运行环境不同,pwnserver支持不同题目使用不同的镜像,只需要在**get_pwn_data**中设置即可 185 | 186 | ### 关于容器 187 | 188 | 对于每个不同的token,都会启动不同的容器,也就是说同一个题会有多个容器。 189 | 190 | 但是对于相同的token则不会启动多个容器 191 | 192 | ### 关于fork bomb 193 | 194 | 每个容器都有资源限制,限制了pid的数量 195 | 196 | 正常情况下父进程被发送SIGPIPE后子进程也会被杀死,所以在socket断开后fork炸弹一般会失效 197 | 198 | 当然,例外情况也是有的,详情见BUG 199 | 200 | ### 关于log 201 | 202 | 对于流量将会被自动记录在log目录下,若是一个连接获取了flag,那条log的文件名中将会包含flag 203 | 204 | 例如: 205 | 206 | ```bash 207 | plusls@pwn:~/pwn/pwn-server/log$ ls -al 208 | total 44 209 | drwxrwxr-x 2 plusls plusls 4096 Aug 18 01:34 . 210 | drwxrwxr-x 9 plusls plusls 4096 Aug 18 01:27 .. 211 | -rw-r--r-- 1 root root 12949 Aug 18 01:34 log.note.note_02.1534527196.572956.log 212 | -rw-r--r-- 1 root root 12237 Aug 18 01:34 log.note.note_02.1534527217.5434923-flag.log 213 | -rw-r--r-- 1 root root 1025 Aug 18 01:31 log.sh.sh.1534527057.2515137.log 214 | -rw-r--r-- 1 root root 507 Aug 18 01:31 log.sh.sh.1534527099.9255965-flag.log 215 | ``` 216 | 217 | 判断一个连接是否成功拿到flag使用了fainotify,想了解实现可以阅读源码 218 | 219 | ### config.json 220 | 221 | pwn-server使用config.json来配置 222 | 223 | ```json 224 | { 225 | // 监听的地址 226 | "address": { 227 | "ip": "0.0.0.0", 228 | "port": 8888 229 | }, 230 | // 尚未使用 231 | "sqlserver": "", 232 | // 放置pwn题的位置 233 | "pwn_dir": "./pwn", 234 | // 存放log的位置 235 | "log_dir": "./log", 236 | // 该目录下存放着xinetd的配置文件 237 | "pwmdocker_dir": "./docker/pwndocker", 238 | // 数据目录 目前只用来存放flag 239 | "data_dir": "./data" 240 | } 241 | ``` 242 | 243 | 244 | 245 | ### TO DO 246 | 247 | ~~1.自动化管理容器~~ 248 | 249 | ~~2.标记出成功getflag的log~~ 250 | 251 | 3.细化容器权限控制 252 | 253 | ### BUG 254 | 255 | 目前已知在某些情况下socket断开后程序仍会继续运行... 256 | 257 | socket断开后程序结束的原因是SIGPIPE,当往一个写端关闭的管道或socket连接中连续写入数据时会引发SIGPIPE信号,从而导致程序结束 258 | 259 | 若是程序无任何写操作并进入死循环,则会出现冗余进程 260 | 261 | 例如 262 | 263 | ```c 264 | #include 265 | #include 266 | #include 267 | #include 268 | 269 | int main() 270 | { 271 | setbuf(stdout, NULL); 272 | setbuf(stdin, NULL); 273 | setbuf(stderr, NULL); 274 | 275 | char s[0x300]; 276 | while (1) { 277 | memset(s, 0, 0x300); 278 | printf("Please input somthing:"); 279 | read(0, s, 0x300); 280 | printf("Your input is:"); 281 | printf(s); 282 | } 283 | return 0; 284 | } 285 | ``` 286 | 287 | printf被改为system后程序就没有任何写操作了,因此不会触发SIGPIPE,从而不会被结束 -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "address": { 3 | "ip": "0.0.0.0", 4 | "port": 8888 5 | }, 6 | "sqlserver": "", 7 | "pwn_dir": "./pwn", 8 | "log_dir": "./log", 9 | "pwmdocker_dir": "./docker/pwndocker", 10 | "data_dir": "./data" 11 | } -------------------------------------------------------------------------------- /docker/pwndocker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM phusion/baseimage:latest 2 | 3 | RUN sed -i "s/http:\/\/archive.ubuntu.com/http:\/\/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list && \ 4 | sed -i "s/http:\/\/security.ubuntu.com/http:\/\/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list && \ 5 | dpkg --add-architecture i386 && \ 6 | apt-get -y update && \ 7 | apt-get -y dist-upgrade && \ 8 | apt-get update && \ 9 | apt-get install -y \ 10 | lib32z1 xinetd build-essential python3 python3-dev libseccomp-dev qemu \ 11 | libc6:i386 libc6-dbg:i386 libc6-dbg lib32stdc++6 g++-multilib --fix-missing && \ 12 | rm -rf /var/lib/apt/list/* 13 | 14 | RUN mkdir /ctf && \ 15 | useradd -b /ctf -m pwn 16 | 17 | EXPOSE 1337 18 | 19 | CMD /ctf/pwn/bin/startdocker.sh 20 | -------------------------------------------------------------------------------- /docker/pwndocker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.2' 2 | 3 | services: 4 | pwn: 5 | build: ./pwndocker 6 | image: cnss/pwn 7 | volumes: 8 | - ./pwndocker/xinetd:/etc/xinetd.d/xinetd:ro 9 | - ./volumes/pwn1/bin:/ctf/bin:ro 10 | - ./volumes/pwn1/flags/abcd/flag:/ctf/pwn/flag:ro 11 | ports: 12 | - "60001:8888" 13 | expose: 14 | - "8888" 15 | pids_limit: 1024 16 | cpus: 0.5 17 | restart: unless-stopped #不管退出状态码是什么始终重启容器 18 | networks: 19 | - pwn 20 | networks: 21 | pwn: 22 | external: 23 | name: pwn -------------------------------------------------------------------------------- /docker/pwndocker/xinetd: -------------------------------------------------------------------------------- 1 | service pwn 2 | { 3 | disable = no 4 | type = UNLISTED 5 | wait = no 6 | server = /bin/sh 7 | # replace helloworld to your program 8 | server_args = -c cd${IFS}/ctf/pwn/bin;exec${IFS}./startpwn.sh 9 | socket_type = stream 10 | protocol = tcp 11 | user = pwn 12 | port = 1337 13 | # bind = 0.0.0.0 14 | # safety options 15 | flags = REUSE 16 | per_source = 10 # the maximum instances of this service per source IP address 17 | rlimit_cpu = 1 # the maximum number of CPU seconds that the service may use 18 | #rlimit_as = 1024M # the Address Space resource limit for the service 19 | #access_times = 2:00-9:00 12:00-24:00 20 | nice = 18 21 | } 22 | -------------------------------------------------------------------------------- /forward.py: -------------------------------------------------------------------------------- 1 | """ 2 | 转发流量 3 | """ 4 | 5 | import logging 6 | import threading 7 | import socket 8 | import time 9 | import os 10 | from watcher import Watcher 11 | from mysocket import myrecv 12 | 13 | PKT_BUFF_SIZE = 4096 14 | 15 | # big brother is watching you 16 | big_brother = None 17 | 18 | 19 | def init_big_brother(data_dir): 20 | global big_brother 21 | big_brother = Watcher(data_dir) 22 | 23 | 24 | # 单向流数据传递 25 | def tcp_mapping_worker(conn_receiver, conn_sender, log_name, token): 26 | logger = logging.getLogger(log_name) 27 | while True: 28 | try: 29 | data = myrecv(conn_receiver, PKT_BUFF_SIZE) 30 | except Exception as e: 31 | logger.info('myrecv error:{} msg: {} Connection closed.'.format(str(type(e)), str(e))) 32 | break 33 | if not data: 34 | logger.info('No more data is received.') 35 | break 36 | try: 37 | conn_sender.sendall(data) 38 | except Exception as e: 39 | logger.info('sendall error:{} msg:{} Connection closed.'.format(str(type(e)), str(e))) 40 | logger.error('Failed sending data.') 41 | break 42 | 43 | # 在socket关闭时调用getpeername可能会抛出异常:ENOTCONN既107 44 | try: 45 | logger.info('{}->{}->{}->{}:\n{}'.format(conn_receiver.getpeername(), 46 | conn_receiver.getsockname(), 47 | conn_sender.getsockname(), 48 | conn_sender.getpeername(), 49 | repr(data))) 50 | except OSError as e: 51 | logger.info('log error:{} msg:{} Connection closed.'.format(str(type(e)), str(e))) 52 | 53 | # shutdown会通知另一个线程结束 54 | try: 55 | conn_receiver.shutdown(socket.SHUT_RDWR) 56 | except Exception: 57 | pass 58 | 59 | try: 60 | conn_sender.shutdown(socket.SHUT_RDWR) 61 | except Exception: 62 | pass 63 | return 64 | 65 | # 端口映射请求处理 66 | 67 | 68 | def tcp_mapping_request(local_conn, remote_ip, remote_port, log_name, log_dir, token, connect_count_dict, lock_dict, timeout_dict, timeout_fun): 69 | '''流量转发+记录''' 70 | global big_brother 71 | logger = logging.getLogger(log_name) 72 | remote_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 73 | for _ in range(20): 74 | try: 75 | remote_conn.connect((remote_ip, remote_port)) 76 | except ConnectionRefusedError as e: 77 | print('ConnectionRefusedError msg:{}'.format(str(e))) 78 | print('try again') 79 | time.sleep(0.2) 80 | continue 81 | except Exception as e: 82 | print('connect error:{} msg:{}'.format(str(type(e)), str(e))) 83 | local_conn.close() 84 | logger.error('Unable to connect to the remote server.') 85 | return 86 | break 87 | else: 88 | # 理论不可能走到这一步 89 | local_conn.close() 90 | logger.error('Unable to connect to the remote server.') 91 | return 92 | 93 | t1 = threading.Thread(target=tcp_mapping_worker, args=( 94 | local_conn, remote_conn, log_name, token)) 95 | t2 = threading.Thread(target=tcp_mapping_worker, args=( 96 | remote_conn, local_conn, log_name, token)) 97 | socket_data = remote_conn.getsockname() 98 | big_brother.add_watch_file(token, socket_data) 99 | t1.start() 100 | t2.start() 101 | t1.join() 102 | t2.join() 103 | file_handle = logger.handlers[0] 104 | logger.info('fd:{} {}'.format(local_conn.fileno(), remote_conn.fileno())) 105 | logger.removeHandler(file_handle) 106 | file_handle.close() 107 | get_flag = big_brother.get_last_access(token, socket_data) 108 | if get_flag: 109 | old_log_path = '{}/{}.log'.format(log_dir, log_name) 110 | new_log_path = '{}/{}-flag.log'.format(log_dir, log_name) 111 | print('big brother catch you') 112 | os.rename(old_log_path, new_log_path) 113 | big_brother.rmv_watch_file(token, socket_data) 114 | 115 | local_conn.close() 116 | remote_conn.close() 117 | 118 | # 设置超时销毁容器 119 | container_lock = lock_dict[token] 120 | container_lock.acquire() 121 | connect_count_dict[token] -= 1 122 | if connect_count_dict[token] == 0: 123 | timer = threading.Timer(5, timeout_fun, args=(token, )) 124 | timeout_dict[token] = timer 125 | timer.start() 126 | print('start timer') 127 | container_lock.release() 128 | return 129 | -------------------------------------------------------------------------------- /get_pwn_data.py: -------------------------------------------------------------------------------- 1 | def get_pwn_data(token): 2 | '''参数为token 返回该token对应的题目名和flag 以及该题的image_tag''' 3 | if token == 'note_01': 4 | return ('note', "cnss{it_is_note_01}", 'cnss/pwn') 5 | elif token == 'note_02': 6 | return ('note', 'cnss{it_is_note_02}', 'cnss/pwn') 7 | if token == 'pwn1_01': 8 | return ('pwn1', "cnss{it_is_pwn1}", 'cnss/pwn') 9 | if token == 'sh': 10 | return ('sh', 'cnss{test_hahah}', 'cnss/pwn') 11 | return('', '', '') -------------------------------------------------------------------------------- /misc.py: -------------------------------------------------------------------------------- 1 | ''' 2 | misc 3 | ''' 4 | import ctypes 5 | import re 6 | 7 | 8 | class TCPData(object): 9 | def __init__(self, s): 10 | s = s.strip() 11 | while True: 12 | tmp = s.replace(' ', '') 13 | if s == tmp: 14 | break 15 | s = tmp 16 | # sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode 17 | # 0: 0B00007F:9F95 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 183745 1 0000000000000000 100 0 0 10 0 18 | result = re.match(r"(\S+): (\S+):(\S+) (\S+):(\S+) (\S+) (\S+):(\S+) (\S+):(\S+) (\S+) (\S+) (\S+)",s).groups() 19 | self.local_address = (self._hex_to_ip(result[1]), int(result[2], 16)) 20 | self.rem_address = (self._hex_to_ip(result[3]), int(result[4], 16)) 21 | self.inode = int(result[12]) 22 | #print(self.local_address, self.rem_address, self.inode) 23 | 24 | def _hex_to_ip(self, hex_str): 25 | # a.b.c.d 26 | d = int(hex_str[0:2], 16) 27 | c = int(hex_str[2:4], 16) 28 | b = int(hex_str[4:6], 16) 29 | a = int(hex_str[6:8], 16) 30 | return '{}.{}.{}.{}'.format(a, b, c, d) 31 | 32 | 33 | def parse_tcp_data(tcp_data_str): 34 | ret = [] 35 | tcp_data_list = tcp_data_str.split('\n')[1:] 36 | for s in tcp_data_list: 37 | if ':' not in s: 38 | continue 39 | tcp_data = TCPData(s) 40 | ret.append(tcp_data) 41 | return ret 42 | 43 | 44 | def get_ppid(pid): 45 | fp = open('/proc/{}/stat'.format(pid), 'rb') 46 | stat = fp.read() 47 | fp.close() 48 | idx = stat.rfind(b')') 49 | ppid = int(stat[idx + 2:].split(b' ')[1]) 50 | return ppid 51 | 52 | def get_cmdline(pid): 53 | fp = open('/proc/{}/cmdline'.format(pid), 'rb') 54 | cmdline = fp.read() 55 | fp.close() 56 | return cmdline 57 | 58 | def get_filename(pid): 59 | fp = open('/proc/{}/stat'.format(pid), 'rb') 60 | stat = fp.read() 61 | fp.close() 62 | idx1 = stat.lfind(b'(') 63 | idx2 = stat.rfind(b')') 64 | filename = stat[idx1 + 1: idx2] 65 | return filename -------------------------------------------------------------------------------- /mysocket.py: -------------------------------------------------------------------------------- 1 | import socket 2 | def myrecv(s, buffersize, flag=0): 3 | while True: 4 | try: 5 | return s.recv(buffersize, flag) 6 | except socket.timeout: 7 | continue 8 | except TimeoutError: 9 | return b'' 10 | 11 | 12 | def recvuntil(connect_socket, s, maxlen=200): 13 | ret = b'' 14 | while True: 15 | #tmp = connect_socket.recv(1000, flags=(socket.MSG_PEEK|socket.MSG_DONTWAIT)) 16 | tmp = myrecv(connect_socket, 1000) 17 | idx = tmp.find(s) 18 | # socket断开时收到'' 不加此判断会导致死循环 19 | if tmp == b'': 20 | return b'' 21 | elif idx != -1: 22 | ret += tmp[:idx + 1] 23 | return ret 24 | elif len(ret) > maxlen: 25 | return '' 26 | 27 | def set_keepalive_linux(sock, after_idle_sec=1, interval_sec=3, max_fails=5): 28 | """Set TCP keepalive on an open socket. 29 | 30 | It activates after 1 second (after_idle_sec) of idleness, 31 | then sends a keepalive ping once every 3 seconds (interval_sec), 32 | and closes the connection after 5 failed ping (max_fails), or 15 seconds 33 | """ 34 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) 35 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, after_idle_sec) 36 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, interval_sec) 37 | sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, max_fails) 38 | 39 | -------------------------------------------------------------------------------- /pwn/note/RNote3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plusls/pwn-server/4717d2c48a8ed57790ce8480031a9367b3f6b63b/pwn/note/RNote3 -------------------------------------------------------------------------------- /pwn/note/startdocker.sh: -------------------------------------------------------------------------------- 1 | /usr/sbin/xinetd -dontfork -------------------------------------------------------------------------------- /pwn/note/startpwn.sh: -------------------------------------------------------------------------------- 1 | ./RNote3 -------------------------------------------------------------------------------- /pwn/pwn1/printf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/plusls/pwn-server/4717d2c48a8ed57790ce8480031a9367b3f6b63b/pwn/pwn1/printf -------------------------------------------------------------------------------- /pwn/pwn1/startdocker.sh: -------------------------------------------------------------------------------- 1 | /usr/sbin/xinetd -dontfork -------------------------------------------------------------------------------- /pwn/pwn1/startpwn.sh: -------------------------------------------------------------------------------- 1 | stdbuf -i 0 -o 0 -e 0 ./printf -------------------------------------------------------------------------------- /pwn/sh/startdocker.sh: -------------------------------------------------------------------------------- 1 | /usr/sbin/xinetd -dontfork -------------------------------------------------------------------------------- /pwn/sh/startpwn.sh: -------------------------------------------------------------------------------- 1 | /bin/sh -------------------------------------------------------------------------------- /pwnserver.py: -------------------------------------------------------------------------------- 1 | import json 2 | import socket 3 | import threading 4 | import logging 5 | import time 6 | import hashlib 7 | import os 8 | import docker 9 | import signal 10 | from get_pwn_data import get_pwn_data 11 | from forward import tcp_mapping_request, init_big_brother 12 | from mysocket import recvuntil, set_keepalive_linux 13 | # log 路径 14 | log_dir = None 15 | # 生成的flag路径 16 | data_dir = None 17 | # pwn二进制文件路径 18 | pwn_dir = None 19 | # pwndocker 路径 包含xinetd dockerfile 20 | pwmdocker_dir = None 21 | # docker_client 22 | docker_client = None 23 | 24 | # 容器列表 25 | container_dict = {} 26 | # connect count dict 27 | connect_count_dict = {} 28 | # timeout dict 29 | timeout_dict = {} 30 | # 锁 31 | lock_dict = {} 32 | connect_lock = None 33 | 34 | def sigint_handler(signum, frame): 35 | print('stop containers') 36 | connect_lock.acquire() 37 | print('lock connect!') 38 | 39 | for token in container_dict: 40 | try: 41 | container_dict[token].kill('SIGKILL') 42 | except: 43 | pass 44 | print('kill {}'.format(token)) 45 | print('kill all container') 46 | docker_client.close() 47 | try: 48 | network = docker_client.networks.get('pwn') 49 | network.remove() 50 | except: 51 | pass 52 | connect_lock.release() 53 | print('exit') 54 | exit(0) 55 | 56 | 57 | def handle_connect(connect_socket): 58 | connect_time = str(time.time()) 59 | set_keepalive_linux(connect_socket) 60 | connect_socket.settimeout(20) 61 | 62 | try: 63 | connect_socket.send(b'input your token:') 64 | token = recvuntil(connect_socket, b'\n')[:-1] 65 | except ConnectionResetError: 66 | print('write input token ConnectionResetError') 67 | connect_socket.close() 68 | return 69 | 70 | try: 71 | token = token.decode() 72 | except UnicodeDecodeError: 73 | print('UnicodeDecodeError token={}'.format(token)) 74 | token = '' 75 | 76 | if token != '': 77 | (problem, flag, image_tag) = get_pwn_data(token) 78 | else: 79 | problem = '' 80 | flag = '' 81 | 82 | if problem == '': 83 | try: 84 | connect_socket.send(b'token error!') 85 | except ConnectionResetError: 86 | print('write token error ConnectionResetError') 87 | except BrokenPipeError: 88 | print('write token error BrokenPipeError') 89 | try: 90 | connect_socket.shutdown(socket.SHUT_RDWR) 91 | except Exception as e: 92 | print('shutdown error:{} msg:{}'.format(str(type(e)), str(e))) 93 | pass 94 | connect_socket.close() 95 | return 96 | 97 | connect_lock.acquire() 98 | #创建锁 99 | if token not in lock_dict: 100 | lock_dict[token] = threading.Lock() 101 | container_lock = lock_dict[token] 102 | #锁定 103 | connect_lock.release() 104 | container_lock.acquire() 105 | 106 | problem_dir = '{}/{}'.format(pwn_dir, problem) 107 | if os.path.isdir(problem_dir) is False: 108 | raise Exception('{} is not exists'.format(problem_dir)) 109 | 110 | # log初始化 111 | log_name = 'log.{}.{}.{}'.format(problem, token, connect_time) 112 | logger = logging.getLogger(log_name) 113 | handler = logging.FileHandler('{}/{}.log'.format(log_dir, log_name)) 114 | handler.setLevel(logging.INFO) 115 | handler.setFormatter(logging.Formatter('[%(asctime)s] %(message)s')) 116 | logger.addHandler(handler) 117 | 118 | logger.info('token={}, problem={}, flag={}, image_tag={}'.format(token, problem, flag, image_tag)) 119 | 120 | # data初始化 121 | problem_data_dir = '{}/{}'.format(data_dir, token) 122 | 123 | # 不存在路径则创建 124 | if os.path.isdir(problem_data_dir) is False: 125 | if os.path.exists(problem_data_dir): 126 | os.remove(problem_data_dir) 127 | os.mkdir(problem_data_dir) 128 | flag_path = '{}/flag'.format(problem_data_dir) 129 | 130 | if os.path.exists(flag_path) is False: 131 | # 写入flag 132 | flag_file = open(flag_path, 'w') 133 | flag_file.write(flag) 134 | flag_file.close() 135 | 136 | # 判断是否存在当前token对应的容器 137 | if token not in container_dict: 138 | try: 139 | pwn_containers = docker_client.containers.get(token) 140 | # 正常来说不可能走到这一步的 141 | if pwn_containers.status == 'exited': 142 | pwn_containers.remove() 143 | pwn_containers = None 144 | except docker.errors.NotFound: 145 | pwn_containers = None 146 | else: 147 | pwn_containers = container_dict[token] 148 | 149 | # 不存在容器则新建容器 150 | if pwn_containers is None: 151 | # 判断是否存在指定镜像 152 | pwn_image = docker_client.images.get(image_tag) 153 | 154 | # 判断是否存在网络 155 | try: 156 | network = docker_client.networks.get('pwn') 157 | except docker.errors.NotFound: 158 | # 可能导致越权访问 159 | network = docker_client.networks.create('pwn') 160 | 161 | volumes = {'{}/xinetd'.format(pwmdocker_dir): {'bind': '/etc/xinetd.d/xinetd', 'mode': 'ro'}, 162 | problem_dir: {'bind': '/ctf/pwn/bin', 'mode': 'ro'}, 163 | flag_path: {'bind': '/ctf/pwn/flag_{}'.format(token), 'mode': 'ro'}, 164 | } 165 | logger.info('create container:{}'.format(token)) 166 | pwn_containers = docker_client.containers.run(image=pwn_image, 167 | #command='sleep infinity', 168 | auto_remove=True, detach=True, 169 | name=token, network='pwn', 170 | pids_limit=30, volumes=volumes) 171 | container_dict[token] = pwn_containers 172 | 173 | # ip 174 | ip = docker_client.api.inspect_container(token)['NetworkSettings']['Networks']['pwn']['IPAddress'] 175 | if ip == '': 176 | logger.info('create container fail, token = {}'.format(token)) 177 | return 178 | logger.info('container ip:{}'.format(ip)) 179 | 180 | 181 | # 记录连接数 182 | if token not in connect_count_dict: 183 | connect_count_dict[token] = 0 184 | connect_count_dict[token] += 1 185 | if token in timeout_dict: 186 | timeout_dict[token].cancel() 187 | del timeout_dict[token] 188 | #释放 189 | container_lock.release() 190 | 191 | tcp_mapping_request(connect_socket, ip, 1337, log_name, log_dir, token, connect_count_dict, lock_dict, timeout_dict, timeout_fun) 192 | 193 | def timeout_fun(token): 194 | container_lock = lock_dict[token] 195 | container_lock.acquire() 196 | if connect_count_dict[token] == 0: 197 | container = container_dict[token] 198 | container.kill('SIGKILL') 199 | print('timeout kill {}'.format(token)) 200 | del timeout_dict[token] 201 | del container_dict[token] 202 | container_lock.release() 203 | 204 | 205 | def main(): 206 | global log_dir, data_dir, pwn_dir, pwmdocker_dir, docker_client, connect_lock 207 | # 连接锁 208 | connect_lock = threading.Lock() 209 | # 超时锁 210 | timeout_lock = threading.Lock() 211 | # 初始化配置 212 | json_fp = open('config.json', 'r') 213 | config = json.loads(json_fp.read()) 214 | json_fp.close() 215 | address = (config['address']['ip'], config['address']['port']) 216 | 217 | # 读取路径配置 218 | log_dir = os.path.abspath(config['log_dir']) 219 | if os.path.isdir(log_dir) is False: 220 | os.mkdir(log_dir) 221 | data_dir = os.path.abspath(config['data_dir']) 222 | if os.path.isdir(data_dir) is False: 223 | os.mkdir(data_dir) 224 | pwn_dir = os.path.abspath(config['pwn_dir']) 225 | if os.path.isdir(pwn_dir) is False: 226 | os.mkdir(pwn_dir) 227 | pwmdocker_dir = os.path.abspath(config['pwmdocker_dir']) 228 | if os.path.isdir(pwmdocker_dir) is False: 229 | os.mkdir(pwmdocker_dir) 230 | 231 | # 初始化logging 232 | logger = logging.getLogger('log') 233 | logger.setLevel(level=logging.INFO) 234 | 235 | # debug 236 | console = logging.StreamHandler() 237 | console.setLevel(logging.INFO) 238 | console.setFormatter(logging.Formatter('[%(asctime)s] %(message)s')) 239 | logger.addHandler(console) 240 | 241 | # 初始化监视器 242 | init_big_brother(data_dir) 243 | 244 | # 初始化docker连接 245 | docker_client = docker.from_env() 246 | 247 | # 处理ctr+c信号 248 | signal.signal(signal.SIGINT, sigint_handler) 249 | 250 | 251 | 252 | server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 253 | # 端口复用 254 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 255 | server_socket.bind(address) 256 | server_socket.listen(100) 257 | while True: 258 | connect_socket, addr = server_socket.accept() 259 | threading.Thread(target=handle_connect, 260 | args=(connect_socket, )).start() 261 | 262 | 263 | if __name__ == '__main__': 264 | main() 265 | -------------------------------------------------------------------------------- /watcher.py: -------------------------------------------------------------------------------- 1 | """ 2 | Big Brother Is Watching you 3 | """ 4 | import threading 5 | import time 6 | import ctypes 7 | import os 8 | import fanotify 9 | from misc import get_ppid, parse_tcp_data, get_cmdline 10 | import docker 11 | 12 | 13 | def IsRootProcess(pid): return os.stat('/proc/{}'.format(pid)).st_uid == 0 14 | 15 | 16 | class Watcher(object): 17 | def __init__(self, data_dir): 18 | self.data_dir = data_dir 19 | self.watch_dict = {} 20 | self.fan_fd = fanotify.Init( 21 | fanotify.FAN_CLASS_PRE_CONTENT, os.O_RDONLY) 22 | self.thread = threading.Thread(target=self._fa_worker) 23 | 24 | # 设置为守护线程 25 | self.thread.setDaemon(True) 26 | self.thread.start() 27 | 28 | def _get_token(self, path): 29 | token = path[path.rfind('flag_') + 5:] 30 | return token # To-Do 31 | 32 | def _get_path(self, token): 33 | return '{}/{}/flag'.format(self.data_dir, token) 34 | 35 | def add_watch_file(self, token, socket_data): 36 | if token not in self.watch_dict: 37 | self.watch_dict[token] = {} 38 | fanotify.Mark(self.fan_fd, 39 | fanotify.FAN_MARK_ADD, 40 | fanotify.FAN_OPEN_PERM, 41 | -1, self._get_path(token)) 42 | self.watch_dict[token][socket_data] = False 43 | 44 | def rmv_watch_file(self, token, socket_data): 45 | del self.watch_dict[token][socket_data] 46 | if len(self.watch_dict[token]) == 0: 47 | fanotify.Mark(self.fan_fd, 48 | fanotify.FAN_MARK_REMOVE, 49 | fanotify.FAN_OPEN_PERM, 50 | -1, self._get_path(token)) 51 | del self.watch_dict[token] 52 | 53 | def get_last_access(self, token, socket_data): 54 | try: 55 | return self.watch_dict[token][socket_data] 56 | except Exception: 57 | print("Error: wrong token.") 58 | return None 59 | 60 | def _fa_worker(self): 61 | while True: 62 | buf = os.read(self.fan_fd, 4096) 63 | assert buf 64 | while fanotify.EventOk(buf): 65 | buf, event = fanotify.EventNext(buf) 66 | if event.mask & fanotify.FAN_Q_OVERFLOW: 67 | print('Queue overflow !') 68 | continue 69 | fdpath = '/proc/self/fd/{:d}'.format(event.fd) 70 | full_path = os.path.abspath(os.readlink(fdpath)) 71 | token = self._get_token(full_path) 72 | if token in self.watch_dict: 73 | # 获取socket数据 74 | socket_data = self._get_socket_data(token, event.pid) 75 | if socket_data != None: 76 | self.watch_dict[token][socket_data] = True 77 | print(socket_data, full_path) # DEBUG 78 | os.write(self.fan_fd, fanotify.Response( 79 | event.fd, fanotify.FAN_ALLOW)) 80 | os.close(event.fd) 81 | assert not buf 82 | 83 | def _get_socket_data(self, token, pid): 84 | '''根据pid确定socket''' 85 | # 连接docker 86 | docker_client = docker.from_env() 87 | pwn_containers = docker_client.containers.get(token) 88 | tcp_data_str = pwn_containers.exec_run( 89 | 'cat /proc/net/tcp').output.decode() 90 | docker_client.close() 91 | # 解析所有tcp连接的数据 92 | tcp_data_list = parse_tcp_data(tcp_data_str) 93 | socket_inode_list = [] 94 | now_pid = pid 95 | # 获取其进程以及父进程的所有socket inode 96 | while True: 97 | dir_list = os.listdir('/proc/{}/fd'.format(now_pid)) 98 | for fd in dir_list: 99 | path = os.readlink('/proc/{}/fd/{}'.format(now_pid, fd)) 100 | if path.startswith('socket:['): 101 | idx1 = path.find('[') 102 | idx2 = path.rfind(']') 103 | socket_inode = int(path[idx1 + 1:idx2]) 104 | if socket_inode not in socket_inode_list: 105 | socket_inode_list.append(socket_inode) 106 | now_pid = get_ppid(now_pid) 107 | if now_pid == 0 or b'/bin/sh\x00-c\x00/ctf/pwn/bin/startdocker.sh\x00' == get_cmdline(now_pid): 108 | break 109 | 110 | for socket_inode in socket_inode_list: 111 | for tcp_data in tcp_data_list: 112 | if tcp_data.inode == socket_inode: 113 | return tcp_data.rem_address 114 | return None 115 | 116 | --------------------------------------------------------------------------------