├── .gitignore ├── LICENSE ├── README.md ├── backends ├── __init__.py ├── dv_farm_flag_submitter.py ├── flag_submitter.py └── hackerdom_flag_submitter.py ├── exploits.examples ├── generator_exploit.py ├── http_exploit.py ├── infinity_exploit.py ├── quick_exploit.py └── tcp_exploit.py ├── exploits └── .empty ├── farm.py ├── farm ├── __init__.py ├── configurator.py ├── defaults.py ├── exploit_storage.py ├── exploits.py ├── farms.py ├── logging.py ├── models.py ├── storage.py └── utils.py ├── requirements.txt ├── settings.py └── teams.csv /.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 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | .idea/ 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andrew Gein, Anastasiya Svalova 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 | Это «ферма» для Attack-Defence CTF-соревнований. Подробнее о соревнованиях можно прочитать [здесь](http://ctftime.org/ctf-wtf). 2 | 3 | Ферма занимается тем, что регулярно запускает написанные эксплойты на все команды, парсит результат работы, достаёт оттуда флаги 4 | и отправляет их в проверяющую систему. Главное отличие этой фермы от остальных — её асинхронность и умение работать 5 | в одном потоке даже при большом количество команд. Кроме того, ферму можно легко расширять: добавлять поддержку новых 6 | проверяющих систем, систем хранения флагов и так далее. 7 | 8 | Ферма написана на третьем питоне, поддерживает версии 3.6 и выше. 9 | 10 | ### Запуск 11 | 12 | 1. Установите зависимости: `pip install -Ur requirements.txt` 13 | 1. Настройте всё необходимое в файле `settings.py`. Список команд положите в `teams.csv` (или другой файл, указанный в настройках) 14 | 1. Запустите ферму: `./farm.py` в линуксе или `python farm.py` в винде 15 | 16 | ### Как написать свой эксплойт 17 | 18 | По умолчанию эксплойты лежат в папке `exploits/`. Ферма автоматически заберёт оттуда эксплойт, если вы оформите его 19 | как класс, унаследованный от `farm.exploits.AbstractExploit`. Примеры эксплойтов лежат в папке `exploits.examples/`. 20 | 21 | **Все эксплойты должны быть асинхронными** 22 | 23 | Это значит, что вы должны использовать асинхронные библиотеки для сетевых запросов: `aiohttp` для HTTP-запросов, 24 | `asyncio` для TCP и так далее. Нельзя использовать синхронные библиотеки (например, `requests` или `pwn`). 25 | 26 | **Не выводите ничего на экран** 27 | 28 | Скорее всего этот вывод потеряется в бесконечности терминала. Если нужно что-нибудь вывести во время работы эксплойта, 29 | используйте методы у `self.logger`: например, `self.logger.info(message)`, `self.logger.error(message, [exception])` и другие. 30 | 31 | Сообщения будут выведены на экран и сохранены в логе (по умолчанию в папке `logs/`). 32 | 33 | **Давайте классам-экслойтам понятные имена** 34 | 35 | По именам этих классам эксплойты различаются в логах. Файлы с логами тоже называются по-разному в зависимости от названий классов. 36 | 37 | **Что должен возвращать эксплойт** 38 | 39 | Ваша единственная задача при написании эксплойта — реализовать асинхронный метод `attack(hostname)`. 40 | Результатом выполнения этого метода должна быть строка (`str` или `bytes`) или массив строк. 41 | 42 | Возвращайте всё, что угодно. Не запрещается возвращать много лишней информации. Главное, чтобы внутри него были флаги. 43 | Флаги будут находиться по регулярному выражению, задающемуся в настройках (`FLAG_FORMAT` в `settings.py`). 44 | 45 | ### Как отладить свой эксплойт 46 | 47 | Положите файл с эксплойтом в корень папки с фермой, добавьте в него следующие строки: 48 | 49 | ``` 50 | if __name__ == '__main__': 51 | from farm.farms import Farm 52 | Farm.debug_exploit(ExploitName(), '127.0.0.1') 53 | ``` 54 | 55 | и запустите. 56 | 57 | Вместо '127.0.0.1' укажите адрес сервера, на котором надо тестировать эксплойт. Вместо `ExploitName()` — имя вашего класса 58 | с эксплойтом. 59 | -------------------------------------------------------------------------------- /backends/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andgein/ctf-exploit-farm/e3062bf9ca06d1eecc607b430faa51dc662171e4/backends/__init__.py -------------------------------------------------------------------------------- /backends/dv_farm_flag_submitter.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import async_timeout 3 | 4 | from backends.flag_submitter import AbstractFlagSubmitter 5 | from farm import models as models 6 | 7 | 8 | class DVFarmFlagSubmitter(AbstractFlagSubmitter): 9 | """ 10 | Flag submitter to Destructive Voice farm server 11 | https://github.com/DestructiveVoice/DestructiveFarm 12 | """ 13 | 14 | TIMEOUT = 10 # For sending one bunch of flags. In seconds 15 | 16 | def __init__(self, host, auth_token=''): 17 | super().__init__() 18 | self._host = host 19 | self.url = self._build_url(host) 20 | self._headers = {} 21 | if auth_token: 22 | self._headers['X-Token'] = auth_token 23 | 24 | async def send_flags(self, flags: [models.Flag]): 25 | for try_index in range(3): 26 | try: 27 | await self._try_send_flags(flags) 28 | except Exception as e: 29 | if try_index < 2: 30 | self.logger.warning( 31 | f'Can\'t send flags to flag server: {e}. ' 32 | f'Let\'s try one more time (it was try {try_index + 1} from 3)' 33 | ) 34 | else: 35 | raise 36 | 37 | for flag in flags: 38 | flag.mark_as_send() 39 | 40 | async def _try_send_flags(self, flags: [models.Flag]): 41 | flags_data = [{'flag': flag.flag, 'sploit': flag.exploit_name, 'team': flag.team} for flag in flags] 42 | 43 | async with aiohttp.ClientSession() as session: 44 | with async_timeout.timeout(self.TIMEOUT): 45 | async with session.post(self.url, headers=self._headers, json=flags_data) as request: 46 | if request.status != 200: 47 | raise Exception(f'Server returned HTTP status code {request.status}, it\'s bad') 48 | 49 | @staticmethod 50 | def _build_url(host): 51 | return f'http://{host}/api/post_flags' 52 | -------------------------------------------------------------------------------- /backends/flag_submitter.py: -------------------------------------------------------------------------------- 1 | from farm.logging import Logger 2 | 3 | 4 | class AbstractFlagSubmitter: 5 | def __init__(self): 6 | self.logger = Logger('FlagSubmitter') 7 | 8 | async def send_flags(self, flags): 9 | raise NotImplementedError('Can\'t send flags: %s doesn\'t specify send_flags(flags)' % self.__class__.__name__) 10 | 11 | 12 | -------------------------------------------------------------------------------- /backends/hackerdom_flag_submitter.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import async_timeout 3 | 4 | from backends.flag_submitter import AbstractFlagSubmitter 5 | from farm import models as models 6 | 7 | 8 | class HackerdomFlagSubmitter(AbstractFlagSubmitter): 9 | """ 10 | Flag submitter for Hackerdom (RuCTF and RuCTFE) checksystem. 11 | See https://github.com/HackerDom/checksystem/blob/master/lib/CS/Controller/Flags.pm or whole repository for details. 12 | """ 13 | 14 | TIMEOUT = 10 # For sending one bunch of flags. In seconds 15 | 16 | def __init__(self, host, team_token): 17 | super().__init__() 18 | self._host = host 19 | self.team_token = team_token 20 | self.url = self._build_url(host) 21 | 22 | self._headers = {'X-Team-Token': self.team_token} 23 | 24 | async def send_flags(self, flags: [models.Flag]): 25 | response = None 26 | for try_index in range(3): 27 | try: 28 | response = await self._try_send_flags(flags) 29 | except Exception as e: 30 | if try_index < 2: 31 | self.logger.warning( 32 | f'Can\'t send flags to checksystem: {e}. ' 33 | f'Let\'s try one more time (it was try {try_index + 1} from 3)' 34 | ) 35 | else: 36 | raise 37 | 38 | flags_results = {flag_info['flag']: flag_info for flag_info in response} 39 | for flag in flags: 40 | if flag.flag not in flags_results: 41 | self.logger.error( 42 | 'Checksystem returned an answer, ' 43 | 'but I can\'t find my flag %s in it. Response is %s' % ( 44 | flag.flag, 45 | flags_results 46 | ) 47 | ) 48 | else: 49 | is_valid = flags_results[flag.flag]['status'] 50 | message = flags_results[flag.flag]['msg'] 51 | if is_valid: 52 | self.logger.info(f"Flag [{flag.flag}] is VALID. Message from checksystem: [{message}]") 53 | flag.mark_as_valid(message) 54 | else: 55 | self.logger.info(f"Flag [{flag.flag}] is INVALID. Message from checksystem: [{message}]") 56 | flag.mark_as_invalid(message) 57 | 58 | async def _try_send_flags(self, flags: [models.Flag]): 59 | flags_data = [flag.flag for flag in flags] 60 | 61 | async with aiohttp.ClientSession() as session: 62 | with async_timeout.timeout(self.TIMEOUT): 63 | async with session.put(self.url, headers=self._headers, json=flags_data) as request: 64 | if request.status >= 300: 65 | raise Exception(f'Checksystem returned HTTP status code {request.status}, it\'s bad') 66 | return await request.json() 67 | 68 | @staticmethod 69 | def _build_url(host): 70 | return 'http://%s/flags' % host 71 | -------------------------------------------------------------------------------- /exploits.examples/generator_exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import asyncio 3 | 4 | from farm.exploits import AbstractExploit 5 | 6 | 7 | class GeneratorExploit(AbstractExploit): 8 | async def attack(self, hostname): 9 | for x in range(3): 10 | await asyncio.sleep(1) 11 | yield str(x) 12 | 13 | 14 | if __name__ == '__main__': 15 | from farm.farms import Farm 16 | Farm.debug_exploit(GeneratorExploit(), '127.0.0.1') 17 | -------------------------------------------------------------------------------- /exploits.examples/http_exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import aiohttp 5 | 6 | from farm.exploits import AbstractExploit 7 | 8 | 9 | class HttpExploit(AbstractExploit): 10 | async def attack(self, hostname): 11 | # cookie_jar=aiohttp.CookieJar(unsafe=True) is needed to correct working on IP (not domain) hostnames 12 | async with aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar(unsafe=True)) as session: 13 | async with session.get('https://en.wikipedia.org/wiki/Dog') as resp: 14 | self.logger.info('HTTP status is %d' % resp.status) 15 | text = await resp.text() 16 | 17 | count = re.findall(r'dog', text) 18 | self.logger.info('Dog exploit: found %d words "dog"' % len(count)) 19 | 20 | return 'Test data U2M3QHS8VCH73R13ALX6R52LCO3E0UJ= flags-flags-flags GUDOITWYX4NSIM88KC23AQRWYDF2MPI=' 21 | 22 | 23 | if __name__ == '__main__': 24 | from farm.farms import Farm 25 | Farm.debug_exploit(HttpExploit(), '127.0.0.1') 26 | -------------------------------------------------------------------------------- /exploits.examples/infinity_exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | 5 | from farm.exploits import AbstractExploit 6 | 7 | 8 | class InfinityExploit(AbstractExploit): 9 | async def attack(self, hostname): 10 | await asyncio.sleep(1000) 11 | return [] 12 | 13 | 14 | if __name__ == '__main__': 15 | from farm.farms import Farm 16 | Farm.debug_exploit(InfinityExploit(), '127.0.0.1') 17 | -------------------------------------------------------------------------------- /exploits.examples/quick_exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from farm.exploits import AbstractExploit 4 | 5 | 6 | class QuickExploit(AbstractExploit): 7 | async def attack(self, hostname): 8 | return ['1WBGKGVEZVULDS4SZ89A10GNPASQTFO='] 9 | 10 | 11 | if __name__ == '__main__': 12 | from farm.farms import Farm 13 | Farm.debug_exploit(QuickExploit(), '127.0.0.1') 14 | -------------------------------------------------------------------------------- /exploits.examples/tcp_exploit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import asyncio 4 | 5 | from farm.exploits import AbstractExploit 6 | 7 | 8 | class TcpExploit(AbstractExploit): 9 | async def attack(self, hostname): 10 | reader, writer = await asyncio.open_connection(hostname, 8888) 11 | 12 | self.logger.info('Send "hello"') 13 | writer.write(b'hello\n') 14 | 15 | line = await reader.read(100) 16 | self.logger.info(f'Received line "{line}"') 17 | 18 | # Don't forget to close connection 19 | writer.close() 20 | 21 | return 'Test data U2M3QHS8VCH73R13ALX6R52LCO3E0UJ= flags-flags-flags GUDOITWYX4NSIM88KC23AQRWYDF2MPI=' 22 | 23 | 24 | if __name__ == '__main__': 25 | from farm.farms import Farm 26 | Farm.debug_exploit(TcpExploit(), '127.0.0.1') 27 | -------------------------------------------------------------------------------- /exploits/.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andgein/ctf-exploit-farm/e3062bf9ca06d1eecc607b430faa51dc662171e4/exploits/.empty -------------------------------------------------------------------------------- /farm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from farm.configurator import Configurator 4 | from farm.farms import Farm 5 | 6 | if __name__ == '__main__': 7 | f = Farm.create_from_configurator(Configurator()) 8 | f.run() 9 | -------------------------------------------------------------------------------- /farm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andgein/ctf-exploit-farm/e3062bf9ca06d1eecc607b430faa51dc662171e4/farm/__init__.py -------------------------------------------------------------------------------- /farm/configurator.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | from farm import utils, defaults 4 | from farm.logging import Logger 5 | from farm.models import Team 6 | 7 | 8 | class Configurator: 9 | DEFAULT_SETTINGS_MODULE_NAME = 'settings' 10 | 11 | def __init__(self, settings_module_name=None): 12 | self._logger = Logger(self) 13 | self.settings_module_name = self.DEFAULT_SETTINGS_MODULE_NAME if settings_module_name is None else settings_module_name 14 | self.settings_module = importlib.import_module(self.settings_module_name) 15 | 16 | def _get_setting_variable(self, setting_name, default_value=None, may_missing=False): 17 | if hasattr(self.settings_module, setting_name): 18 | return getattr(self.settings_module, setting_name) 19 | if may_missing: 20 | return default_value 21 | raise ValueError('Specify variable %s in settings file (%s)' % (setting_name, self.settings_module_name)) 22 | 23 | def _get_setting_object(self, setting_name, may_missing=False): 24 | object_spec = self._get_setting_variable(setting_name, may_missing=may_missing) 25 | if object_spec is None: 26 | return None 27 | 28 | # If you don't need args and kwargs, 29 | # you can use just 'module.name.class_name' instead of { 'type': 'module.name.class_name' } 30 | if type(object_spec) is str: 31 | object_spec = {'type': object_spec} 32 | 33 | if type(object_spec) is not dict: 34 | raise ValueError('Variable %s in settings file (%s) should be dict or str, not %s' % ( 35 | setting_name, 36 | self.settings_module_name, 37 | type(object_spec) 38 | )) 39 | object_type_name = self._get_dict_value(object_spec, 'type', setting_name) 40 | object_args = object_spec.get('args', ()) 41 | object_kwargs = object_spec.get('kwargs', {}) 42 | 43 | try: 44 | object_type = utils.import_type(object_type_name) 45 | except ValueError as e: 46 | raise ValueError('Can not find type %s for initializing %s: %s' % ( 47 | object_type_name, 48 | setting_name, 49 | e 50 | )) 51 | 52 | self._logger.info('Creating %s with arguments %s and kwarguments %s' % ( 53 | object_type.__name__, 54 | object_args, 55 | object_kwargs 56 | )) 57 | return object_type(*object_args, **object_kwargs) 58 | 59 | def _get_dict_value(self, dict_object, param, setting_name): 60 | try: 61 | return dict_object[param] 62 | except KeyError: 63 | raise ValueError('Variable %s in settings file (%s) should has key "%s"' % ( 64 | setting_name, 65 | self.settings_module_name, 66 | param 67 | )) 68 | 69 | def get_flag_format(self): 70 | return self._get_setting_variable('FLAG_FORMAT') 71 | 72 | def get_flag_submitter(self): 73 | return self._get_setting_object('FLAG_SUBMITTER') 74 | 75 | def get_teams(self): 76 | teams = self._get_setting_variable('TEAMS') 77 | return list(Team(*team) for team in teams) 78 | 79 | def get_exploit_storage(self): 80 | return self._get_setting_object('EXPLOIT_STORAGE') 81 | 82 | def get_flag_storage(self): 83 | return self._get_setting_object('FLAG_STORAGE') 84 | 85 | def get_round_timeout(self): 86 | return self._get_setting_variable('ROUND_TIMEOUT', defaults.ROUND_TIMEOUT, may_missing=True) 87 | 88 | def get_submitter_sleep(self): 89 | return self._get_setting_variable('SUBMITTER_SLEEP', defaults.SUBMITTER_SLEEP, may_missing=True) 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /farm/defaults.py: -------------------------------------------------------------------------------- 1 | # Default values for some settings 2 | 3 | ROUND_TIMEOUT = 60 # In seconds 4 | 5 | SUBMITTER_SLEEP = 5 # In seconds 6 | 7 | FLAG_STORAGE_DIRECTORY = 'flags' -------------------------------------------------------------------------------- /farm/exploit_storage.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os.path 3 | 4 | from farm import utils 5 | from farm.exploits import AbstractExploit 6 | from farm.logging import Logger 7 | 8 | 9 | class AbstractExploitStorage: 10 | def __init__(self): 11 | self.logger = Logger('ExploitStorage') 12 | 13 | async def get_exploits(self): 14 | raise NotImplementedError() 15 | 16 | 17 | class DirectoryExploitStorage(AbstractExploitStorage): 18 | def __init__(self, directory): 19 | super().__init__() 20 | self.directory = directory 21 | 22 | async def get_exploits(self): 23 | self.logger.info('Loading exploits from %s' % os.path.abspath(self.directory)) 24 | AbstractExploit.clear_subclasses() 25 | for file_name in glob.glob(self.directory + '/**/*.py', recursive=True): 26 | self.logger.info('Try to load exploits from %s' % file_name) 27 | try: 28 | utils.import_module_from_file(file_name) 29 | except Exception as e: 30 | raise Exception('Can not load exploit from %s: %s' % (file_name, e), e) 31 | 32 | exploits_classes = AbstractExploit.get_all_subclasses() 33 | self.logger.info( 34 | 'Found %d exploit classes: [%s]' % ( 35 | len(exploits_classes), 36 | ', '.join(c.__name__ for c in exploits_classes) 37 | ) 38 | ) 39 | return [exploit_class() for exploit_class in exploits_classes] 40 | 41 | 42 | class OneExploitStorage(AbstractExploitStorage): 43 | def __init__(self, exploit): 44 | super().__init__() 45 | self.exploit = exploit 46 | 47 | async def get_exploits(self): 48 | return [self.exploit] -------------------------------------------------------------------------------- /farm/exploits.py: -------------------------------------------------------------------------------- 1 | from farm.logging import Logger 2 | 3 | 4 | class AbstractExploit: 5 | _subclasses = [] 6 | 7 | def __init__(self): 8 | self.logger = Logger(self, 'exploits') 9 | # For backward compatibility: 10 | self._logger = self.logger 11 | 12 | async def attack(self, hostname): 13 | raise NotImplementedError('Can\'t attack host %s: %s doesn\'t specify attack(hostname)' % (hostname, self.__class__.__name__)) 14 | 15 | def __init_subclass__(cls, **kwargs): 16 | AbstractExploit._subclasses.append(cls) 17 | 18 | @classmethod 19 | def clear_subclasses(cls): 20 | cls._subclasses.clear() 21 | 22 | @classmethod 23 | def get_all_subclasses(cls): 24 | return cls._subclasses 25 | 26 | def __str__(self): 27 | return self.__class__.__name__ 28 | -------------------------------------------------------------------------------- /farm/farms.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import re 3 | import traceback 4 | import types 5 | 6 | import async_timeout 7 | 8 | import farm.models as models 9 | from backends.flag_submitter import AbstractFlagSubmitter 10 | from farm import defaults 11 | from farm.configurator import Configurator 12 | from farm.exploit_storage import AbstractExploitStorage, OneExploitStorage 13 | from farm.logging import Logger 14 | from farm.storage import AbstractFlagStorage 15 | 16 | 17 | class Farm: 18 | def __init__(self, 19 | teams: [models.Team], 20 | exploit_storage: AbstractExploitStorage, 21 | flag_submitter: AbstractFlagSubmitter, 22 | flag_storage: AbstractFlagStorage, 23 | flag_format: str, 24 | round_timeout=None, 25 | submitter_sleep=None): 26 | self.teams = teams 27 | self.exploit_storage = exploit_storage 28 | self.flag_submitter = flag_submitter 29 | self.flag_storage = flag_storage 30 | self.flag_format = flag_format 31 | self._flag_re_str = re.compile(flag_format) 32 | self._flag_re_bytes = re.compile(flag_format.encode()) 33 | self.round_timeout = defaults.ROUND_TIMEOUT if round_timeout is None else round_timeout 34 | self.submitter_sleep = defaults.SUBMITTER_SLEEP if submitter_sleep is None else submitter_sleep 35 | 36 | self.logger = Logger(self) 37 | 38 | @classmethod 39 | def create_from_configurator(cls, configurator: Configurator): 40 | return cls( 41 | configurator.get_teams(), 42 | configurator.get_exploit_storage(), 43 | configurator.get_flag_submitter(), 44 | configurator.get_flag_storage(), 45 | configurator.get_flag_format(), 46 | configurator.get_round_timeout(), 47 | configurator.get_submitter_sleep(), 48 | ) 49 | 50 | @classmethod 51 | def debug_exploit(cls, exploit, vulnbox, loop=None): 52 | if loop is None: 53 | loop = asyncio.get_event_loop() 54 | 55 | configurator = Configurator() 56 | farm = cls( 57 | [models.Team('Target', vulnbox)], 58 | OneExploitStorage(exploit), 59 | None, 60 | None, 61 | configurator.get_flag_format(), 62 | ) 63 | task = asyncio.wait([farm._run_one_round()], loop=loop) 64 | loop.run_until_complete(task) 65 | 66 | def run(self, loop=None): 67 | if loop is None: 68 | loop = asyncio.get_event_loop() 69 | 70 | task = asyncio.wait([self._run_exploits(), self._run_submitter()], loop=loop) 71 | loop.run_until_complete(task) 72 | 73 | async def _run_exploits(self): 74 | round = 0 75 | while True: 76 | round += 1 77 | try: 78 | self.logger.info('EXPLOITS ROUND %d' % round) 79 | await self._run_one_round() 80 | except asyncio.TimeoutError as e: 81 | # It's ok: just some exploit didn't finish him work 82 | pass 83 | except Exception as e: 84 | self.logger.error('Error occurred while running exploits: %s' % e, e) 85 | 86 | async def _run_one_round(self): 87 | try: 88 | exploits = await self.exploit_storage.get_exploits() 89 | except Exception as e: 90 | self.logger.error(f'Can\'t get exploits from exploits storage: {e}', e) 91 | return 92 | 93 | if not exploits: 94 | # It no exploits loaded sleep self.round_timeout 95 | await asyncio.sleep(self.round_timeout) 96 | return 97 | 98 | with async_timeout.timeout(self.round_timeout): 99 | tasks = [] 100 | for team in self.teams: 101 | for exploit in exploits: 102 | tasks.append(self._run_exploit(team, exploit)) 103 | 104 | while tasks: 105 | done, tasks = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) 106 | for task in done: 107 | exploit_result, exploit_name, team = await task 108 | # TODO (andgein): dump exploit output somewhere 109 | flags = self._extract_flags(exploit_result) 110 | if len(flags) > 0: 111 | self.logger.info('Found flags in exploit output: [%s]' % ", ".join(map(str, flags))) 112 | 113 | if self.flag_storage is not None: 114 | await self.flag_storage.add_flags(flags, exploit_name, team.name) 115 | 116 | async def _run_exploit(self, team, exploit): 117 | exploit_name = str(exploit) 118 | try: 119 | self.logger.info(f'Start exploit [{exploit}] on team [{team}]') 120 | run_attack = exploit.attack(team.vulnbox) 121 | # In case of async generators (https://www.python.org/dev/peps/pep-0525/) we can't await it, we should 122 | # iterate over it via `async for` operator 123 | if isinstance(run_attack, types.AsyncGeneratorType): 124 | return [data async for data in run_attack], exploit_name, team 125 | 126 | # Otherwise just await it because `exploit.attack()` is asynchronous function 127 | return await run_attack, exploit_name, team 128 | except Exception as e: 129 | message = f'Exception on exploit [{exploit}] on team [{team}]: {e}' 130 | self.logger.warning(message) 131 | # Duplicate message in exploit's log with stacktrace details and ERROR level 132 | exploit.logger.error(message + "\n" + traceback.format_exc()) 133 | return '', exploit_name, team 134 | 135 | def _extract_flags(self, exploit_result): 136 | if type(exploit_result) in [str, bytes]: 137 | exploit_result = [exploit_result] 138 | 139 | # TODO (andgein): log which exploit it was 140 | if not isinstance(exploit_result, (tuple, list, types.GeneratorType)): 141 | error_message = 'Exploit returned invalid result: %s, should be tuple, list, generator, str or bytes, not %s' % ( 142 | exploit_result, type(exploit_result) 143 | ) 144 | self.logger.error(error_message) 145 | return [] 146 | 147 | flags = [] 148 | for line in exploit_result: 149 | if type(line) is bytes: 150 | line_flags = re.findall(self._flag_re_bytes, line) 151 | elif type(line) is str: 152 | line_flags = re.findall(self._flag_re_str, line) 153 | else: 154 | self.logger.error( 155 | 'Exploit returned invalid result in list: %s, should be str or bytes, not %s' % ( 156 | line, 157 | type(line) 158 | )) 159 | line_flags = [] 160 | flags.extend(line_flags) 161 | 162 | return flags 163 | 164 | async def _run_submitter(self): 165 | while True: 166 | try: 167 | flags = await self.flag_storage.get_not_sent_flags() 168 | except Exception as e: 169 | self.logger.error(f'Can\'t get not sent flags from storage: {e}', e) 170 | continue 171 | 172 | if len(flags) > 0: 173 | self.logger.info( 174 | f'Got {len(flags)} not sent flags from flag storage and ' 175 | f'sending them to checksystem: [{", ".join(f.flag for f in flags)}]' 176 | ) 177 | try: 178 | await self.flag_submitter.send_flags(flags) 179 | except Exception as e: 180 | self.logger.error(f'Can\'t send flags to checksystem: {e}', e) 181 | 182 | await asyncio.sleep(self.submitter_sleep) 183 | -------------------------------------------------------------------------------- /farm/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import os.path 4 | import coloredlogs 5 | 6 | import farm.utils as utils 7 | 8 | 9 | class Logger: 10 | LOGS_DIRECTORY = 'logs' 11 | 12 | def __init__(self, name, sub_directory=''): 13 | self._ensure_logs_directory_exists(os.path.join(self.LOGS_DIRECTORY, sub_directory)) 14 | 15 | if type(name) is not str: 16 | name = name.__class__.__name__ 17 | 18 | self._logger = logging.getLogger(name) 19 | if len(self._logger.handlers) > 0: 20 | return 21 | 22 | self._logger.setLevel(logging.DEBUG) 23 | 24 | stream_handler = logging.StreamHandler() 25 | stream_handler.setFormatter(coloredlogs.ColoredFormatter('[%(name)s] [%(levelname)s] %(message)s')) 26 | self._logger.addHandler(stream_handler) 27 | 28 | file_name = utils.camel_case_to_underscore(name) + '.log' 29 | file_path = os.path.join(self.LOGS_DIRECTORY, sub_directory, file_name) 30 | file_handler = logging.FileHandler(file_path) 31 | file_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')) 32 | self._logger.addHandler(file_handler) 33 | 34 | def debug(self, message): 35 | self._logger.debug(message) 36 | 37 | def info(self, message): 38 | self._logger.info(message) 39 | 40 | def warning(self, message): 41 | self._logger.warning(message) 42 | 43 | def error(self, message, exception: Exception=None): 44 | self._logger.error(message) 45 | if exception is not None: 46 | self._logger.exception(exception) 47 | 48 | @staticmethod 49 | def _ensure_logs_directory_exists(directory): 50 | if not os.path.exists(directory): 51 | os.makedirs(directory) 52 | 53 | if os.path.isfile(directory): 54 | raise ValueError(f'Can not write logs into directory {directory}, because it\'s file') 55 | -------------------------------------------------------------------------------- /farm/models.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import enum 3 | import datetime 4 | 5 | 6 | class Team(collections.namedtuple('Team', ('name', 'vulnbox'))): 7 | def __str__(self): 8 | return f'{self.name} on {self.vulnbox}' 9 | 10 | 11 | class Flag: 12 | def __init__(self, flag, exploit_name='', team=''): 13 | self.flag = flag 14 | self.created_at = datetime.datetime.now() 15 | self.exploit_name = exploit_name 16 | self.team = team 17 | self.status = FlagStatus.DONT_SEND 18 | self.status_message = '' 19 | 20 | def update_status(self, status, status_message=''): 21 | self.status = status 22 | self.status_message = status_message 23 | 24 | def mark_as_send(self): 25 | self.update_status(FlagStatus.SEND) 26 | 27 | def mark_as_valid(self, message=''): 28 | self.update_status(FlagStatus.VALID, message) 29 | 30 | def mark_as_invalid(self, message=''): 31 | self.update_status(FlagStatus.INVALID, message) 32 | 33 | def __str__(self): 34 | return '%s by %s from %s (found at %s, status is %s%s)' % ( 35 | self.flag, 36 | self.exploit_name, 37 | self.team, 38 | self.created_at, 39 | self.status, 40 | ', ' + self.status_message if self.status_message != '' else '' 41 | ) 42 | 43 | 44 | class FlagStatus(enum.Enum): 45 | DONT_SEND = 1, 46 | SEND = 2, 47 | VALID = 3, 48 | INVALID = 4 49 | -------------------------------------------------------------------------------- /farm/storage.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import collections 4 | 5 | import farm.models as models 6 | from farm import defaults 7 | from farm.logging import Logger 8 | 9 | 10 | class AbstractFlagStorage: 11 | async def add_flags(self, flags: [str], exploit_name: str, team: str): 12 | if len(flags) == 0: 13 | return 14 | await asyncio.wait([self.add_flag(models.Flag(flag, exploit_name, team)) for flag in flags]) 15 | 16 | async def add_flag(self, flag: models.Flag): 17 | raise NotImplementedError('Can\'t add flag to storage: %s doesn\'t specify add_flag(flag)' % self.__class__.__name__) 18 | 19 | async def get_not_sent_flags(self, limit=30): 20 | raise NotImplementedError('Can\'t get not sent flag from storage: %s doesn\'t specify get_not_sent_flags(flag)' % self.__class__.__name__) 21 | 22 | 23 | class DirectoryFlagStorage(AbstractFlagStorage): 24 | def __init__(self, directory=defaults.FLAG_STORAGE_DIRECTORY): 25 | self._logger = Logger(self) 26 | 27 | self.directory = directory 28 | self._current_flags = collections.deque() 29 | self._all_flags = set() 30 | 31 | async def add_flag(self, flag: models.Flag): 32 | if flag.flag not in self._all_flags: 33 | self._current_flags.append(flag) 34 | self._logger.info(f'Added flag [{flag}] to the storage. Current size of storage is {len(self._current_flags)}') 35 | self._all_flags.add(flag.flag) 36 | else: 37 | self._logger.debug(f'Flag [{flag}] is duplicating, don\'t add it to the storage') 38 | 39 | async def get_not_sent_flags(self, limit=30): 40 | not_sent_flags = [] 41 | while self._current_flags and len(not_sent_flags) < limit: 42 | flag = self._current_flags.popleft() 43 | if flag.status == models.FlagStatus.DONT_SEND: 44 | not_sent_flags.append(flag) 45 | 46 | # Continue monitoring not sent flags, but add them to the end of queue 47 | self._current_flags.extend(not_sent_flags) 48 | # TODO (andgein): Write not fetched flags to file 49 | return not_sent_flags 50 | -------------------------------------------------------------------------------- /farm/utils.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import importlib 3 | import importlib.util 4 | import re 5 | 6 | __all__ = [ 7 | 'camel_case_to_underscore', 8 | 'import_type', 'import_module_from_file', 9 | 'read_teams_from_file', 10 | ] 11 | 12 | 13 | first_cap_re = re.compile('(.)([A-Z][a-z]+)') 14 | all_cap_re = re.compile('([a-z0-9])([A-Z])') 15 | 16 | 17 | def camel_case_to_underscore(name: str): 18 | s1 = first_cap_re.sub(r'\1_\2', name) 19 | return all_cap_re.sub(r'\1_\2', s1).lower() 20 | 21 | 22 | def import_type(name: str): 23 | if name.count('.') < 1: 24 | raise ValueError('Type name should contains dot: "%s", because otherwise I can not split it into module name and type name' % name) 25 | 26 | module_name, type_name = name.rsplit('.', 1) 27 | module = importlib.import_module(module_name) 28 | 29 | if not hasattr(module, type_name): 30 | raise ValueError('Can not find type %s in module %s' % (type_name, module)) 31 | 32 | return getattr(module, type_name) 33 | 34 | 35 | def import_module_from_file(path: str): 36 | spec = importlib.util.spec_from_file_location("module", path) 37 | module = importlib.util.module_from_spec(spec) 38 | spec.loader.exec_module(module) 39 | return module 40 | 41 | 42 | def read_teams_from_file(file_name: str): 43 | """ 44 | Reads teams from CSV file. File should be in UTF-8 and has two columns: team name and vulnbox address. 45 | Common usage is `TEAMS = read_teams_from_file('teams.csv')` in `settings.py` 46 | :param file_name: name of CSV file 47 | :return: list of pairs 48 | """ 49 | from farm.logging import Logger 50 | 51 | logger = Logger('Configurator') 52 | logger.info('Reading teams list from %s' % file_name) 53 | result = [] 54 | with open(file_name, 'r', newline='', encoding='utf-8') as f: 55 | csv_reader = csv.reader(f) 56 | for line_index, row in enumerate(csv_reader): 57 | if len(row) != 2: 58 | raise ValueError('Invalid teams files %s: row #%d "%s" contains more or less than 2 items' % ( 59 | file_name, 60 | line_index, 61 | ','.join(row) 62 | )) 63 | 64 | result.append(tuple(row)) 65 | logger.info('Read %d teams from %s' % (len(result), file_name)) 66 | return result 67 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | aiodns 3 | async_timeout 4 | cchardet 5 | coloredlogs -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | from farm.utils import read_teams_from_file 2 | 3 | # Specify regular expression for valid flags 4 | FLAG_FORMAT = r'[A-Z0-9]{31}=' 5 | 6 | # Flag submitter for some checksystem 7 | FLAG_SUBMITTER = { 8 | # HackerdomFlagSubmitter is submitter for Hackerdom's CTFs such as RuCTF or RuCTFE 9 | 'type': 'backends.hackerdom_flag_submitter.HackerdomFlagSubmitter', 10 | 'kwargs': { 11 | 'host': '', 12 | 'team_token': '', 13 | } 14 | } 15 | 16 | # Teams list 17 | # List of pairs (Team name, Vulnbox address). 18 | # Teams list can be retrieved from csv file via `utils.read_teams_from_file('teams.csv')` 19 | TEAMS = read_teams_from_file('teams.csv') 20 | 21 | ## 22 | # All following settings are okay to be used as-is. Change each of them only if you fully understand what does it mean 23 | ## 24 | 25 | # Flag storage: remember all found flags, allow to don't resend them 26 | FLAG_STORAGE = 'farm.storage.DirectoryFlagStorage' 27 | 28 | # Exploit storage 29 | EXPLOIT_STORAGE = { 30 | # DirectoryExploitStorage read all exploits from directory (exploits/) before round starts. 31 | # Just put file with exploit into this directory and farm will see it. No need to restart farm for each new exploit. 32 | 'type': 'farm.exploit_storage.DirectoryExploitStorage', 33 | 'kwargs': { 34 | 'directory': 'exploits', 35 | } 36 | } 37 | 38 | # Round is step for running all exploits to all teams. 39 | # You can think about round timeout as a timeout for each exploit. 40 | # Round timeout is specified in seconds. Default value is 60 41 | 42 | # ROUND_TIMEOUT = 60 43 | 44 | # Sleep interval between two flag submitter's tries. In seconds. Default value is 5 45 | 46 | # SUBMITTER_SLEEP = 5 47 | -------------------------------------------------------------------------------- /teams.csv: -------------------------------------------------------------------------------- 1 | First team,127.0.0.1 2 | Second team,127.0.0.2 3 | Third team,127.0.0.3 --------------------------------------------------------------------------------