diff --git a/.travis.yml b/.travis.yml index 36c7bac7..4da78c04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,6 @@ +sudo: required +services: + - docker language: python python: - "3.5" diff --git a/requirements.txt b/requirements.txt index 522b47fa..17cdf731 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ aiohttp>=2.0 aiomysql -elizabeth +docker +elizabeth==0.3.27 yarl redis asyncio_redis diff --git a/tanner/config.py b/tanner/config.py index 10bc6bd3..bcf3307d 100644 --- a/tanner/config.py +++ b/tanner/config.py @@ -11,6 +11,7 @@ 'REDIS': {'host': 'localhost', 'port': 6379, 'poolsize': 80, 'timeout': 1}, 'EMULATORS': {'root_dir': '/opt/tanner'}, 'SQLI': {'type':'SQLITE', 'db_name': 'tanner_db', 'host':'localhost', 'user':'root', 'password':'user_pass'}, + 'CMD_EXEC': {'host_image': 'busybox:latest'}, 'LOGGER': {'log_debug': '/opt/tanner/tanner.log', 'log_err': '/opt/tanner/tanner.err'}, 'MONGO': {'enabled': 'False', 'URI': 'mongodb://localhost'}, 'LOCALLOG': {'enabled': 'False', 'PATH': '/tmp/tanner_report.json'}, diff --git a/tanner/emulators/base.py b/tanner/emulators/base.py index c6be2f72..d0ebcf83 100644 --- a/tanner/emulators/base.py +++ b/tanner/emulators/base.py @@ -3,7 +3,7 @@ import urllib.parse import yarl -from tanner.emulators import lfi, rfi, sqli, xss +from tanner.emulators import lfi, rfi, sqli, xss, cmd_exec from tanner.utils import patterns @@ -20,7 +20,8 @@ def __init__(self, base_dir, db_name, loop=None): 'rfi': rfi.RfiEmulator(base_dir, loop), 'lfi': lfi.LfiEmulator(base_dir), 'xss': xss.XssEmulator(), - 'sqli': sqli.SqliEmulator(db_name, base_dir) + 'sqli': sqli.SqliEmulator(db_name, base_dir), + 'cmd_exec': cmd_exec.CmdExecEmulator() } async def handle_post(self, session, data): @@ -33,6 +34,12 @@ async def handle_post(self, session, data): if sqli_data: sqli_result = await self.emulators['sqli'].handle(sqli_data, session, 1) detection = {'name': 'sqli', 'order': 2, 'payload': sqli_result} + else: + cmd_exec_data = await self.emulators['cmd_exec'].check_post_data(data) + if cmd_exec_data: + cmd_exec_results = await self.emulators['cmd_exec'].handle(cmd_exec_data[0][1], session) + detection = {'name': 'cmd_exec', 'order': 3, 'payload': cmd_exec_results} + return detection async def handle_get(self, session, path): @@ -54,10 +61,15 @@ async def handle_get(self, session, path): attack_value = value if detection['order'] <= 1: - sqli = self.emulators['sqli'].check_get_data(path) - if sqli: - detection = {'name': 'sqli', 'order': 2} - attack_value = path + cmd_exec = await self.emulators['cmd_exec'].check_get_data(path) + if cmd_exec: + detection = {'name': 'cmd_exec', 'order': 3} + attack_value = cmd_exec[0][1] + else: + sqli = self.emulators['sqli'].check_get_data(path) + if sqli: + detection = {'name': 'sqli', 'order': 2} + attack_value = path if detection['name'] in self.emulators: emulation_result = await self.emulators[detection['name']].handle(attack_value, session) diff --git a/tanner/emulators/cmd_exec.py b/tanner/emulators/cmd_exec.py new file mode 100644 index 00000000..c6634f96 --- /dev/null +++ b/tanner/emulators/cmd_exec.py @@ -0,0 +1,93 @@ +import asyncio +import docker +import yarl +# TODO : Replace docker with aiodocker +import logging + +from tanner.config import TannerConfig +from tanner.utils import patterns + +class CmdExecEmulator: + def __init__(self): + try: + self.docker_client = docker.from_env(version='auto') + except docker.errors as docker_error: + self.logger.error('Error while connecting to docker service %s', docker_error) + self.host_image = TannerConfig.get('CMD_EXEC', 'host_image') + self.logger = logging.getLogger('tanner.cmd_exec_emulator.CmdExecEmulator') + + async def setup_host_image(self): + try: + if not self.docker_client.images.list(self.host_image): + self.docker_client.images.pull(self.host_image) + except docker.errors as docker_error: + self.logger.error('Error while pulling %s image %s', self.host_image, docker_error) + + async def get_container(self, container_name): + container = None + try: + container_if_exists = self.docker_client.containers.list(all= True, + filters= dict(name= container_name) + ) + if container_if_exists: + container = container_if_exists[0] + except docker.errors.APIError as server_error: + self.logger.error('Error while fetching container list %s', server_error) + return container + + async def create_attacker_env(self, session): + await self.setup_host_image() + container_name = 'attacker_' + session.sess_uuid.hex + container = await self.get_container(container_name) + if not container: + try: + container = self.docker_client.containers.create(image= self.host_image, + stdin_open= True, + name= container_name + ) + session.associate_env(container_name) + except docker.errors as docker_error: + self.logger.error('Error while creating a container %s', docker_error) + return container + + async def get_cmd_exec_results(self, container, cmd): + execute_result = None + try: + container.start() + execute_result = container.exec_run(['sh', '-c', cmd]).decode('utf-8') + container.kill() + except docker.errors.APIError as server_error: + self.logger.error('Error while executing command %s in container %s', cmd, server_error) + result = dict(value= execute_result, page= '/index.html') + return result + + async def delete_env(self, container_name): + container = await self.get_container(container_name) + try: + if container: + container.remove(force = True) + except docker.errors.APIError as server_error: + self.logger.error('Error while removing container %s', server_error) + + async def check_post_data(self, data): + cmd_data = [] + for (param_id, param_value) in data['post_data'].items(): + if patterns.CMD_ATTACK.match(param_value): + cmd_data.append((param_id, param_value)) + return cmd_data + + async def check_get_data(self, path): + cmd_data = [] + query = yarl.URL(path).query_string + params = query.split('&') + for param in params: + if len(param.split('=')) == 2: + param_id, param_value = param.split('=') + if patterns.CMD_ATTACK.match(param_value): + cmd_data.append((param_id, param_value)) + return cmd_data + + async def handle(self, value, session= None): + container = await self.create_attacker_env(session) + result = await self.get_cmd_exec_results(container, value) + return result \ No newline at end of file diff --git a/tanner/session.py b/tanner/session.py index 8dfd397d..0f144605 100644 --- a/tanner/session.py +++ b/tanner/session.py @@ -1,9 +1,11 @@ +import asyncio import json import time import asyncio import uuid from tanner.config import TannerConfig +from tanner.emulators import cmd_exec from tanner.utils.mysql_db_helper import MySQLDBHelper from tanner.utils.sqlite_db_helper import SQLITEDBHelper @@ -20,6 +22,7 @@ def __init__(self, data): 'response_status': data['status']}] self.cookies = data['cookies'] self.associated_db = None + self.associated_env = None except KeyError: raise @@ -68,5 +71,11 @@ async def remove_associated_db(self): else: SQLITEDBHelper().delete_db(self.associated_db) + def associate_env(self, env): + self.associated_env = env + + async def remove_associated_env(self): + await cmd_exec.CmdExecEmulator().delete_env(self.associated_env) + def get_uuid(self): return str(self.sess_uuid) diff --git a/tanner/session_manager.py b/tanner/session_manager.py index ad50cffc..ad7aac73 100644 --- a/tanner/session_manager.py +++ b/tanner/session_manager.py @@ -71,6 +71,8 @@ async def delete_old_sessions(self, redis_client): if not sess.is_expired(): continue await sess.remove_associated_db() + sess.remove_associated_db() + await sess.remove_associated_env() self.sessions.remove(sess) try: await redis_client.set(sess.get_uuid(), sess.to_json()) diff --git a/tanner/tests/test_config.py b/tanner/tests/test_config.py index 8c611016..020fac11 100644 --- a/tanner/tests/test_config.py +++ b/tanner/tests/test_config.py @@ -4,7 +4,6 @@ from tanner import config - class TestCongif(unittest.TestCase): def setUp(self): config.TannerConfig.config = None @@ -16,6 +15,7 @@ def setUp(self): 'REDIS': {'host': 'localhost', 'port': '1337', 'poolsize': '40', 'timeout': '5'}, 'EMULATORS': {'root_dir': '/tmp/user_tanner'}, 'SQLI': {'type':'SQLITE', 'db_name': 'user_tanner_db', 'host':'localhost', 'user':'user_name', 'password':'user_pass'}, + 'CMD_EXEC': {'host_image': 'test_image'}, 'LOGGER': {'log_debug': '/opt/tanner/tanner.log', 'log_err': '/opt/tanner/tanner.err'}, 'MONGO': {'enabled': 'False', 'URI': 'mongodb://localhost'}, 'LOCALLOG': {'enabled': 'False', 'PATH': '/tmp/user_tanner_report.json'} @@ -60,6 +60,7 @@ def test_get_when_file_dont_exists(self): 'REDIS': {'host': 'localhost', 'port': 6379, 'poolsize': 80, 'timeout': 1}, 'EMULATORS': {'root_dir': '/opt/tanner'}, 'SQLI': {'type':'SQLITE', 'db_name': 'tanner_db', 'host':'localhost', 'user':'root', 'password':'user_pass'}, + 'CMD_EXEC': {'host_image': 'busybox:latest'}, 'LOGGER': {'log_debug': '/opt/tanner/tanner.log', 'log_err': '/opt/tanner/tanner.err'}, 'MONGO': {'enabled': 'False', 'URI': 'mongodb://localhost'}, 'LOCALLOG': {'enabled': 'False', 'PATH': '/tmp/tanner_report.json'} diff --git a/tanner/utils/patterns.py b/tanner/utils/patterns.py index 56cb15b0..d801ae8d 100644 --- a/tanner/utils/patterns.py +++ b/tanner/utils/patterns.py @@ -6,6 +6,7 @@ LFI_ATTACK = re.compile('.*(\/\.\.)*(home|proc|usr|etc)\/.*') LFI_FILEPATH = re.compile('((\.\.|\/).*)') XSS_ATTACK = re.compile('.*<(.|\n)*?>') +CMD_ATTACK = re.compile('.*(alias|cat|cd|cp|echo|exec|find|for|grep|ifconfig|ls|man|mkdir|netstat|ping|ps|pwd|uname|wget|touch|while).*') REMOTE_FILE_URL = re.compile('(.*(http(s){0,1}|ftp(s){0,1}):.*)') WORD_PRESS_CONTENT = re.compile('\/wp-content\/.*') HTML_TAGS = re.compile('.*<(.*)>.*')