Skip to content

Commit

Permalink
Command exec emulator (#144)
Browse files Browse the repository at this point in the history
* Add CMD identifier pattern

* CmdExec emulator

* update DOCKER in requirements

* add DOCKER service to travis

* Use host-image from config

* fix when image is unavailable

* change default host_image

* add logging and error catching

* fix tests

* change calling function location

* Catch docker service exception

* Add TODO

* Support nested commands

* add get & post support using different approach

* fix merge conflicts

* change indentation to spaces

* remove unnecessary files
  • Loading branch information
rnehra01 authored and afeena committed Jun 4, 2017
1 parent 369618a commit 6beb627
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 8 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
sudo: required
services:
- docker
language: python
python:
- "3.5"
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
aiohttp>=2.0
aiomysql
elizabeth
docker
elizabeth==0.3.27
yarl
redis
asyncio_redis
Expand Down
1 change: 1 addition & 0 deletions tanner/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'},
Expand Down
24 changes: 18 additions & 6 deletions tanner/emulators/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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)
Expand Down
93 changes: 93 additions & 0 deletions tanner/emulators/cmd_exec.py
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions tanner/session.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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)
2 changes: 2 additions & 0 deletions tanner/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
3 changes: 2 additions & 1 deletion tanner/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

from tanner import config


class TestCongif(unittest.TestCase):
def setUp(self):
config.TannerConfig.config = None
Expand All @@ -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'}
Expand Down Expand Up @@ -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'}
Expand Down
1 change: 1 addition & 0 deletions tanner/utils/patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('.*<(.*)>.*')
Expand Down

0 comments on commit 6beb627

Please sign in to comment.