Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
afeena committed Jun 8, 2017
2 parents a036090 + cc769fd commit 799e699
Show file tree
Hide file tree
Showing 36 changed files with 1,073 additions and 701 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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ Getting Started
1. For PHP Sandbox setup, see sandbox [manual] (https://github.com/mushorg/phpox)
2. In PHP Sandbox directory, run sandbox: ``sudo python3 sandbox.py``

### Setup Docker


1. Install [docker](https://docs.docker.com/engine/installation/linux/ubuntu/)
2. Pull the required image to use [default : ``busybox:latest``]

### Setup and run TANNER


Expand Down
3 changes: 2 additions & 1 deletion bin/tanner
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ def main():
print("Error logs will be stored in", error_log_file_name)
if TannerConfig.get('LOCALLOG', 'enabled') == 'True':
print("Data logs will be stored in", TannerConfig.get('LOCALLOG', 'PATH'))
tanner.server.run_server()
tanner = server.TannerServer()
tanner.start()


if __name__ == "__main__":
Expand Down
13 changes: 11 additions & 2 deletions docs/source/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,16 @@ There are 8 different sections :

:root_dir: The root directory for emulators that need data storing such as SQLI and LFI. Data will be stored in this directory
* **SQLI**

:db_name: THe name of database used in SQLI emulator
:type: Supports two types MySQL/SQLITE
:db_name: The name of database used in SQLI emulator
:host: This will be used for MySQL to get the host address
:user: This is the MySQL user which perform DB queries
:password: The password corresponding to the above user
* **CMD_EXEC**

:host_image: The image which emulates commands in Command Execution Emulator
* **LOGGER**

:log_file: Location of tanner log file
Expand All @@ -47,7 +55,8 @@ If no file is specified, following json will be used as default:
'TANNER': {'host': '0.0.0.0', 'port': 8090},
'REDIS': {'host': 'localhost', 'port': 6379, 'poolsize': 80, 'timeout': 1},
'EMULATORS': {'root_dir': '/opt/tanner'},
'SQLI': {'db_name': 'tanner.db'},
'SQLI': {'type':'SQLITE', 'db_name': 'tanner_db', 'host':'localhost', 'user':'root', 'password':'user_pass'},
'CMD_EXEC': {'host_image': 'busybox:latest'},
'LOGGER': {'log_file': '/opt/tanner/tanner.log'},
'MONGO': {'enabled': 'False', 'URI': 'mongodb://localhost'},
'LOCALLOG': {'enabled': 'False', 'PATH': '/tmp/tanner_report.json'}
Expand Down
19 changes: 19 additions & 0 deletions docs/source/emulators.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,30 @@ It emulates `SQL injection`_ vulnerability. This attack is detected by ``libinje
The emulator copies the original database (see :doc:`db_setup` for more info about db) to a dummy database for every attacker.
It uses UUID of the session for the attacker's db name. Every query is executed on the attacker's db.
The emulator returns the result of the execution and the page where SNARE should show the result.
It supports two types of DBs.
* **SQLITE**
To enable it, set SQLI type to SQLITE in config
* **MySQL**
To enable it, set SQLI type to MySQL in config and set other necessary fields - Host, User and Password

Command Execution Emulator
~~~~~~~~~~~~~~~~~~~~~~~~~~

It emulates `Command Execution`_ vulnerability. This attack is detected with pattern.

::

.*(alias|cat|cd|cp|echo|exec|find|for|grep|ifconfig|ls|man|mkdir|netstat|ping|ps|pwd|uname|wget|touch|while).*

* Each param value is checked against the pattern and ``command`` is extracted.
* The ``command`` is executed in a docker container safely.
* Results from container is injected into the index page.


.. _RFI: https://en.wikipedia.org/wiki/File_inclusion_vulnerability#Remote_File_Inclusion
.. _PHPox: https://github.com/mushorg/phpox
.. _LFI: https://en.wikipedia.org/wiki/File_inclusion_vulnerability#Local_File_Inclusion
.. _XSS: https://en.wikipedia.org/wiki/Cross-site_scripting
.. _SQL injection: https://en.wikipedia.org/wiki/SQL_injection
.. _Command Execution: https://www.owasp.org/index.php/Command_Injection
.. _manual: https://github.com/client9/libinjection/wiki/doc-sqli-python
6 changes: 4 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
aiohttp<2.0
elizabeth
aiohttp>=2.0
aiomysql
docker
elizabeth==0.3.27
yarl
redis
asyncio_redis
Expand Down
28 changes: 10 additions & 18 deletions tanner/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import asyncio
import json
import logging
from urllib.parse import urlparse, parse_qs

import asyncio_redis

Expand All @@ -10,34 +8,28 @@ class Api:
def __init__(self):
self.logger = logging.getLogger('tanner.api.Api')

@asyncio.coroutine
def handle_api_request(self, path, redis_client):
async def handle_api_request(self, query, params, redis_client):
result = None

parsed_path = urlparse(path)
query = parse_qs(parsed_path.query)

if parsed_path.path.startswith('/api/stats') and not query:
result = yield from self.return_stats(redis_client)
elif parsed_path.path == '/api/stats' and 'uuid' in query:
result = yield from self.return_uuid_stats(query['uuid'], redis_client, 50)
if query == 'stats' and not params:
result = await self.return_stats(redis_client)
elif query == 'stats' and 'uuid' in params:
result = await self.return_uuid_stats(params['uuid'], redis_client, 50)
return result

@asyncio.coroutine
def return_stats(self, redis_client):
async def return_stats(self, redis_client):
query_res = []
try:
query_res = yield from redis_client.smembers('snare_ids')
query_res = yield from query_res.asset()
query_res = await redis_client.smembers('snare_ids')
query_res = await query_res.asset()
except asyncio_redis.NotConnectedError as connection_error:
self.logger.error('Can not connect to redis %s', connection_error)
return list(query_res)

@asyncio.coroutine
def return_uuid_stats(self, uuid, redis_client, count=-1):
async def return_uuid_stats(self, uuid, redis_client, count=-1):
query_res = []
try:
query_res = yield from redis_client.lrange_aslist(uuid[0], 0, count)
query_res = await redis_client.lrange_aslist(uuid, 0, count)
except asyncio_redis.NotConnectedError as connection_error:
self.logger.error('Can not connect to redis %s', connection_error)
else:
Expand Down
3 changes: 2 additions & 1 deletion tanner/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
'TANNER': {'host': '0.0.0.0', 'port': 8090},
'REDIS': {'host': 'localhost', 'port': 6379, 'poolsize': 80, 'timeout': 1},
'EMULATORS': {'root_dir': '/opt/tanner'},
'SQLI': {'db_name': 'tanner.db'},
'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
60 changes: 28 additions & 32 deletions tanner/dorks_manager.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import asyncio
import logging
import math
import os
import pickle
import random
import re
import uuid
import math

import asyncio_redis

from tanner.utils import patterns
from tanner import config
from tanner.utils import patterns


class DorksManager:
dorks_key = uuid.uuid3(uuid.NAMESPACE_DNS, 'dorks').hex
Expand All @@ -21,8 +21,7 @@ def __init__(self):
self.init_done = False

@staticmethod
@asyncio.coroutine
def push_init_dorks(file_name, redis_key, redis_client):
async def push_init_dorks(file_name, redis_key, redis_client):
dorks = None
if os.path.exists(file_name):
with open(file_name, 'rb') as dorks_file:
Expand All @@ -32,57 +31,54 @@ def push_init_dorks(file_name, redis_key, redis_client):
dorks = dorks.split()
if isinstance(dorks, set):
dorks = [x for x in dorks if x is not None]
yield from redis_client.sadd(redis_key, dorks)
await redis_client.sadd(redis_key, dorks)

@asyncio.coroutine
def extract_path(self, path, redis_client):
async def extract_path(self, path, redis_client):
extracted = re.match(patterns.QUERY, path)
if extracted:
extracted = extracted.group(0)

try:
yield from redis_client.sadd(self.user_dorks_key, [extracted])
await redis_client.sadd(self.user_dorks_key, [extracted])
except asyncio_redis.NotConnectedError as connection_error:
self.logger('Problem with redis connection: %s', connection_error)
self.logger.error('Problem with redis connection: %s', connection_error)

@asyncio.coroutine
def init_dorks(self, redis_client):
async def init_dorks(self, redis_client):
try:
transaction = yield from redis_client.multi()
dorks_exist = yield from transaction.exists(self.dorks_key)
user_dorks_exist = yield from transaction.exists(self.user_dorks_key)
transaction = await redis_client.multi()
dorks_exist = await transaction.exists(self.dorks_key)
user_dorks_exist = await transaction.exists(self.user_dorks_key)

yield from transaction.exec()
await transaction.exec()
except (asyncio_redis.TransactionError, asyncio_redis.NotConnectedError) as redis_error:
self.logger('Problem with transaction: %s', redis_error)
self.logger.error('Problem with transaction: %s', redis_error)
else:
dorks_existed = yield from dorks_exist
user_dorks_existed = yield from user_dorks_exist
dorks_existed = await dorks_exist
user_dorks_existed = await user_dorks_exist

if not dorks_existed:
yield from self.push_init_dorks(config.TannerConfig.get('DATA', 'dorks'), self.dorks_key, redis_client)
await self.push_init_dorks(config.TannerConfig.get('DATA', 'dorks'), self.dorks_key, redis_client)
if not user_dorks_existed:
yield from self.push_init_dorks(config.TannerConfig.get('DATA', 'user_dorks'), self.user_dorks_key, redis_client)
await self.push_init_dorks(config.TannerConfig.get('DATA', 'user_dorks'), self.user_dorks_key,
redis_client)

self.init_done = True

@asyncio.coroutine
def choose_dorks(self, redis_client):
async def choose_dorks(self, redis_client):
if not self.init_done:
yield from self.init_dorks(redis_client)
await self.init_dorks(redis_client)
chosen_dorks = []
max_dorks = 50
try:
transaction = yield from redis_client.multi()
dorks = yield from transaction.smembers_asset(self.dorks_key)
user_dorks = yield from transaction.smembers_asset(self.user_dorks_key)
transaction = await redis_client.multi()
dorks = await transaction.smembers_asset(self.dorks_key)
user_dorks = await transaction.smembers_asset(self.user_dorks_key)

yield from transaction.exec()
await transaction.exec()
except (asyncio_redis.TransactionError, asyncio_redis.NotConnectedError) as redis_error:
self.logger('Problem with transaction: %s', redis_error)
self.logger.error('Problem with transaction: %s', redis_error)
else:
dorks = yield from dorks
user_dorks = yield from user_dorks
dorks = await dorks
user_dorks = await user_dorks
chosen_dorks.extend(random.sample(dorks, random.randint(math.floor(0.5 * max_dorks), max_dorks)))
try:
if max_dorks > len(user_dorks):
Expand Down
54 changes: 31 additions & 23 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 @@ -15,29 +15,34 @@ class BaseHandler:
patterns.XSS_ATTACK: dict(name='xss', order=3)
}

def __init__(self, base_dir, db_name):
def __init__(self, base_dir, db_name, loop=None):
self.emulators = {
'rfi': rfi.RfiEmulator(base_dir),
'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()
}

@asyncio.coroutine
def handle_post(self, session, data):
async def handle_post(self, session, data):
detection = dict(name='unknown', order=0)
xss_result = yield from self.emulators['xss'].handle(None, session, data)
xss_result = await self.emulators['xss'].handle(None, session, data)
if xss_result:
detection = {'name': 'xss', 'order': 2, 'payload': xss_result}
else:
sqli_data = yield from self.emulators['sqli'].check_post_data(data)
sqli_data = self.emulators['sqli'].check_post_data(data)
if sqli_data:
sqli_result = yield from self.emulators['sqli'].handle(sqli_data, session, 1)
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

@asyncio.coroutine
def handle_get(self, session, path):
async def handle_get(self, session, path):
detection = dict(name='unknown', order=0)
# dummy for wp-content
if re.match(patterns.WORD_PRESS_CONTENT, path):
Expand All @@ -56,27 +61,30 @@ def handle_get(self, session, path):
attack_value = value

if detection['order'] <= 1:
sqli = yield from 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 = yield from self.emulators[detection['name']].handle(attack_value, session)
emulation_result = await self.emulators[detection['name']].handle(attack_value, session)
detection['payload'] = emulation_result

return detection

@asyncio.coroutine
def emulate(self, data, session, path):
async def emulate(self, data, session, path):
if data['method'] == 'POST':
detection = yield from self.handle_post(session, data)
detection = await self.handle_post(session, data)
else:
detection = yield from self.handle_get(session, path)
detection = await self.handle_get(session, path)

return detection

@asyncio.coroutine
def handle(self, data, session, path):
detection = yield from self.emulate(data, session, path)
async def handle(self, data, session, path):
detection = await self.emulate(data, session, path)
return detection
Loading

0 comments on commit 799e699

Please sign in to comment.