ftpot 2.0.0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- core/__init__.py +0 -0
- core/config.py +50 -0
- core/logfile.py +74 -0
- core/output.py +39 -0
- core/paths.py +53 -0
- core/protocol.py +284 -0
- core/tools.py +170 -0
- ftpot/__init__.py +25 -0
- ftpot/cli.py +513 -0
- ftpot/data/Dockerfile +56 -0
- ftpot/data/docs/INSTALL.md +441 -0
- ftpot/data/docs/INSTALLWIN.md +454 -0
- ftpot/data/docs/PLUGINS.md +21 -0
- ftpot/data/docs/TODO.md +8 -0
- ftpot/data/docs/datadog/README.md +32 -0
- ftpot/data/docs/discord/README.md +58 -0
- ftpot/data/docs/geoipupdtask.ps1 +270 -0
- ftpot/data/docs/mysql/README.md +176 -0
- ftpot/data/docs/mysql/READMEWIN.md +157 -0
- ftpot/data/docs/mysql/mysql.sql +85 -0
- ftpot/data/docs/postgres/README.md +184 -0
- ftpot/data/docs/postgres/READMEWIN.md +196 -0
- ftpot/data/docs/postgres/postgres.sql +73 -0
- ftpot/data/docs/slack/README.md +68 -0
- ftpot/data/docs/sqlite3/README.md +131 -0
- ftpot/data/docs/sqlite3/READMEWIN.md +123 -0
- ftpot/data/docs/sqlite3/sqlite3.sql +69 -0
- ftpot/data/docs/telegram/README.md +103 -0
- ftpot/data/etc/honeypot.cfg.base +414 -0
- ftpot/data/test/.gitignore +6 -0
- ftpot/data/test/README.md +40 -0
- ftpot/data/test/baseline +613 -0
- ftpot/data/test/input +474 -0
- ftpot/data/test/test.py +78 -0
- ftpot/honeypot.py +114 -0
- ftpot-2.0.0.dist-info/METADATA +154 -0
- ftpot-2.0.0.dist-info/RECORD +63 -0
- ftpot-2.0.0.dist-info/WHEEL +6 -0
- ftpot-2.0.0.dist-info/entry_points.txt +2 -0
- ftpot-2.0.0.dist-info/licenses/LICENSE +674 -0
- ftpot-2.0.0.dist-info/top_level.txt +3 -0
- output_plugins/__init__.py +0 -0
- output_plugins/couch.py +68 -0
- output_plugins/datadog.py +74 -0
- output_plugins/discord.py +138 -0
- output_plugins/elastic.py +137 -0
- output_plugins/hpfeed.py +43 -0
- output_plugins/influx2.py +66 -0
- output_plugins/jsonlog.py +36 -0
- output_plugins/kafka.py +57 -0
- output_plugins/localsyslog.py +66 -0
- output_plugins/mongodb.py +83 -0
- output_plugins/mysql.py +213 -0
- output_plugins/nlcvapi.py +120 -0
- output_plugins/postgres.py +155 -0
- output_plugins/redisdb.py +47 -0
- output_plugins/rethinkdblog.py +46 -0
- output_plugins/slack.py +94 -0
- output_plugins/socketlog.py +40 -0
- output_plugins/sqlite.py +144 -0
- output_plugins/telegram.py +141 -0
- output_plugins/textlog.py +46 -0
- output_plugins/xmpp.py +193 -0
core/__init__.py
ADDED
|
File without changes
|
core/config.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
|
|
2
|
+
from configparser import ConfigParser, ExtendedInterpolation
|
|
3
|
+
|
|
4
|
+
from os import environ
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def to_environ_key(key):
|
|
8
|
+
return key.upper()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class EnvironmentConfigParser(ConfigParser):
|
|
12
|
+
|
|
13
|
+
def has_option(self, section, option):
|
|
14
|
+
if to_environ_key('_'.join((section, option))) in environ:
|
|
15
|
+
return True
|
|
16
|
+
return super(EnvironmentConfigParser, self).has_option(section, option)
|
|
17
|
+
|
|
18
|
+
def get(self, section, option, **kwargs):
|
|
19
|
+
key = to_environ_key('_'.join((section, option)))
|
|
20
|
+
if key in environ:
|
|
21
|
+
return environ[key]
|
|
22
|
+
return super(EnvironmentConfigParser, self).get(section, option, **kwargs)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def readConfigFile(cfgfile):
|
|
26
|
+
"""
|
|
27
|
+
Read config files and return ConfigParser object
|
|
28
|
+
|
|
29
|
+
@param cfgfile: filename or array of filenames
|
|
30
|
+
@return: ConfigParser object
|
|
31
|
+
"""
|
|
32
|
+
parser = EnvironmentConfigParser(
|
|
33
|
+
interpolation=ExtendedInterpolation(),
|
|
34
|
+
converters={'list': lambda x: [i.strip() for i in x.split(',')]}
|
|
35
|
+
)
|
|
36
|
+
parser.read(cfgfile)
|
|
37
|
+
return parser
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _config_files():
|
|
41
|
+
# Import here (not at module top) to avoid any circular-import risk.
|
|
42
|
+
from core.paths import workdir_path, bundled
|
|
43
|
+
return [
|
|
44
|
+
bundled('etc', 'honeypot.cfg.base'), # bundled read-only defaults
|
|
45
|
+
workdir_path('etc', 'honeypot.cfg'), # site-local overrides
|
|
46
|
+
workdir_path('honeypot.cfg'), # convenience root-level override
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
CONFIG = readConfigFile(_config_files())
|
core/logfile.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
from sys import stdout
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from pytz import timezone
|
|
6
|
+
|
|
7
|
+
from twisted.python import log, util
|
|
8
|
+
from twisted.python.logfile import DailyLogFile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HoneypotDailyLogFile(DailyLogFile):
|
|
12
|
+
"""
|
|
13
|
+
Overload original Twisted with improved date formatting
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def suffix(self, tupledate):
|
|
17
|
+
"""
|
|
18
|
+
Return the suffix given a (year, month, day) tuple or unixtime
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
return "{:02d}-{:02d}-{:02d}".format(tupledate[0], tupledate[1], tupledate[2])
|
|
22
|
+
except Exception:
|
|
23
|
+
# try taking a float unixtime
|
|
24
|
+
return '_'.join(map(str, self.toDate(tupledate)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def myFLOemit(self, eventDict):
|
|
28
|
+
"""
|
|
29
|
+
Format the given log event as text and write it to the output file.
|
|
30
|
+
|
|
31
|
+
@param eventDict: a log event
|
|
32
|
+
@type eventDict: L{dict} mapping L{str} (native string) to L{object}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Custom emit for FileLogObserver
|
|
36
|
+
text = log.textFromEventDict(eventDict)
|
|
37
|
+
if text is None:
|
|
38
|
+
return
|
|
39
|
+
timeStr = self.formatTime(eventDict['time'])
|
|
40
|
+
fmtDict = {
|
|
41
|
+
'text': text.replace('\n', '\n\t')
|
|
42
|
+
}
|
|
43
|
+
msgStr = log._safeFormat('%(text)s\n', fmtDict)
|
|
44
|
+
util.untilConcludes(self.write, timeStr + ' ' + msgStr)
|
|
45
|
+
util.untilConcludes(self.flush)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def myFLOformatTime(self, when):
|
|
49
|
+
"""
|
|
50
|
+
Log time in UTC
|
|
51
|
+
|
|
52
|
+
By default it's formatted as an ISO8601-like string (ISO8601 date and
|
|
53
|
+
ISO8601 time separated by a space). It can be customized using the
|
|
54
|
+
C{timeFormat} attribute, which will be used as input for the underlying
|
|
55
|
+
L{datetime.datetime.strftime} call.
|
|
56
|
+
|
|
57
|
+
@type when: C{int}
|
|
58
|
+
@param when: POSIX (ie, UTC) timestamp.
|
|
59
|
+
|
|
60
|
+
@rtype: C{str}
|
|
61
|
+
"""
|
|
62
|
+
timeFormatString = self.timeFormat
|
|
63
|
+
if timeFormatString is None:
|
|
64
|
+
timeFormatString = '[%Y-%m-%d %H:%M:%S.%fZ]'
|
|
65
|
+
return datetime.fromtimestamp(when, tz=timezone('UTC')).strftime(timeFormatString)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def set_logger(cfg_options):
|
|
69
|
+
log.FileLogObserver.emit = myFLOemit
|
|
70
|
+
log.FileLogObserver.formatTime = myFLOformatTime
|
|
71
|
+
if cfg_options['logfile'] is None:
|
|
72
|
+
log.startLogging(stdout)
|
|
73
|
+
else:
|
|
74
|
+
log.startLogging(HoneypotDailyLogFile.fromFullPath(cfg_options['logfile']), setStdout=False)
|
core/output.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
|
|
2
|
+
from socket import gethostname
|
|
3
|
+
|
|
4
|
+
from core.config import CONFIG
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Output(object):
|
|
8
|
+
"""
|
|
9
|
+
Abstract base class intended to be inherited by output plugins.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, general_options):
|
|
13
|
+
|
|
14
|
+
self.cfg = general_options
|
|
15
|
+
|
|
16
|
+
if 'sensor' in self.cfg:
|
|
17
|
+
self.sensor = self.cfg['sensor']
|
|
18
|
+
else:
|
|
19
|
+
self.sensor = CONFIG.get('honeypot', 'sensor_name', fallback=gethostname())
|
|
20
|
+
|
|
21
|
+
self.start()
|
|
22
|
+
|
|
23
|
+
def start(self):
|
|
24
|
+
"""
|
|
25
|
+
Abstract method to initialize output plugin
|
|
26
|
+
"""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def stop(self):
|
|
30
|
+
"""
|
|
31
|
+
Abstract method to shut down output plugin
|
|
32
|
+
"""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def write(self, event):
|
|
36
|
+
"""
|
|
37
|
+
Handle a general event within the output plugin
|
|
38
|
+
"""
|
|
39
|
+
pass
|
core/paths.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
paths.py - Single source of truth for runtime path resolution.
|
|
3
|
+
|
|
4
|
+
The honeypot needs a "working directory" containing:
|
|
5
|
+
etc/ config files (honeypot-launch.cfg.base, honeypot.cfg.base)
|
|
6
|
+
data/ geolocation databases, SQLite db, etc.
|
|
7
|
+
log/ rotating log files (created on demand)
|
|
8
|
+
|
|
9
|
+
Priority for locating the working directory:
|
|
10
|
+
1. FTPOT_WORKDIR environment variable
|
|
11
|
+
2. Current working directory
|
|
12
|
+
|
|
13
|
+
The bundled read-only defaults (etc/honeypot.cfg.base) are installed inside the
|
|
14
|
+
`ftpot` package and located via the package's own __file__ attribute, which
|
|
15
|
+
works on all Python versions without requiring pkg_resources or
|
|
16
|
+
importlib.resources.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import absolute_import
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
from os import getcwd, environ
|
|
23
|
+
from os.path import abspath, dirname, join
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_workdir():
|
|
27
|
+
"""Return the absolute path to the runtime working directory."""
|
|
28
|
+
env = environ.get('FTPOT_WORKDIR', '').strip()
|
|
29
|
+
if env:
|
|
30
|
+
return abspath(env)
|
|
31
|
+
return getcwd()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def workdir_path(*parts):
|
|
35
|
+
"""Return an absolute path rooted at the working directory."""
|
|
36
|
+
return join(get_workdir(), *parts)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def bundled(*parts):
|
|
40
|
+
"""
|
|
41
|
+
Return the filesystem path to a file bundled inside the installed package.
|
|
42
|
+
Arguments are path components relative to the ftpot/data/ directory,
|
|
43
|
+
passed as separate strings (like os.path.join) to avoid hardcoded separators.
|
|
44
|
+
|
|
45
|
+
Uses the package's own __file__ to locate the data directory, which works
|
|
46
|
+
on all Python versions (2.7+) without requiring pkg_resources or
|
|
47
|
+
importlib.resources.
|
|
48
|
+
"""
|
|
49
|
+
# ftpot/data/ lives alongside this module's package (core/ is a sibling
|
|
50
|
+
# of ftpot/), so we go up one level from core/ to find ftpot/data/.
|
|
51
|
+
here = dirname(abspath(__file__))
|
|
52
|
+
package_dir = join(dirname(here), 'ftpot')
|
|
53
|
+
return join(package_dir, 'data', *parts)
|
core/protocol.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import absolute_import
|
|
3
|
+
|
|
4
|
+
from ipaddress import ip_address, ip_network
|
|
5
|
+
from sys import version_info
|
|
6
|
+
from time import time
|
|
7
|
+
from uuid import uuid4
|
|
8
|
+
|
|
9
|
+
from core.tools import (
|
|
10
|
+
decode,
|
|
11
|
+
encode,
|
|
12
|
+
get_local_ip,
|
|
13
|
+
get_utc_time,
|
|
14
|
+
printable,
|
|
15
|
+
write_event
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from twisted.internet.ssl import PrivateCertificate
|
|
19
|
+
from twisted.internet.protocol import ServerFactory
|
|
20
|
+
from twisted.protocols.basic import LineReceiver
|
|
21
|
+
from twisted.python.log import msg
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if version_info[0] >= 3:
|
|
25
|
+
def unicode(x):
|
|
26
|
+
return x
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MyFTPServer(LineReceiver):
|
|
30
|
+
def __init__(self, options):
|
|
31
|
+
self.cfg = options
|
|
32
|
+
self.userReceived = False
|
|
33
|
+
self.user = ''
|
|
34
|
+
self.is_fttps = False
|
|
35
|
+
|
|
36
|
+
def connectionMade(self):
|
|
37
|
+
self.session = uuid4().hex[:12]
|
|
38
|
+
self.report_event('connect')
|
|
39
|
+
self.send_line('220 ProFTPD 1.3.4b Server (TP-Share) [{}]'.format(self.cfg['public_ip']))
|
|
40
|
+
|
|
41
|
+
def lineReceived(self, line):
|
|
42
|
+
line = decode(line)
|
|
43
|
+
parts = line.split(None, 1)
|
|
44
|
+
if parts:
|
|
45
|
+
command = parts[0].upper()
|
|
46
|
+
args = parts[1] if len(parts) > 1 else ''
|
|
47
|
+
self.report_event('command', command, args)
|
|
48
|
+
self.process_command(command, args)
|
|
49
|
+
|
|
50
|
+
def connectionLost(self, reason):
|
|
51
|
+
self.report_event('disconnect', reason.value)
|
|
52
|
+
self.session = None
|
|
53
|
+
self.userReceived = False
|
|
54
|
+
self.user = ''
|
|
55
|
+
self.is_fttps = False
|
|
56
|
+
|
|
57
|
+
def report_event(self, operation, username=None, password=None):
|
|
58
|
+
operation = operation.lower()
|
|
59
|
+
unix_time = time()
|
|
60
|
+
peer = self.transport.getPeer()
|
|
61
|
+
ip = peer.host
|
|
62
|
+
for network in self.cfg['blacklist']:
|
|
63
|
+
if ip_address(unicode(ip)) in ip_network(unicode(network)):
|
|
64
|
+
return
|
|
65
|
+
port = peer.port
|
|
66
|
+
event = {
|
|
67
|
+
'eventid': 'ftpot.' + operation,
|
|
68
|
+
'operation': operation,
|
|
69
|
+
'timestamp': get_utc_time(unix_time),
|
|
70
|
+
'unixtime': unix_time,
|
|
71
|
+
'src_ip': ip,
|
|
72
|
+
'src_port': port,
|
|
73
|
+
'dst_port': self.cfg['port'],
|
|
74
|
+
'sensor': self.cfg['sensor'],
|
|
75
|
+
'dst_ip': self.cfg['public_ip'] if self.cfg['report_public_ip'] else get_local_ip(),
|
|
76
|
+
'session': self.session
|
|
77
|
+
}
|
|
78
|
+
if operation == 'login':
|
|
79
|
+
event['username'] = printable(username)
|
|
80
|
+
event['password'] = printable(password)
|
|
81
|
+
message = 'Login with username: "{}", password: "{}" from {}:{}.'.format(
|
|
82
|
+
event['username'],
|
|
83
|
+
event['password'],
|
|
84
|
+
ip,
|
|
85
|
+
port
|
|
86
|
+
)
|
|
87
|
+
elif operation == 'connect':
|
|
88
|
+
message = 'Connection made from {}:{}.'.format(ip, port)
|
|
89
|
+
elif operation == 'command':
|
|
90
|
+
command = printable(username)
|
|
91
|
+
event['command'] = command
|
|
92
|
+
if password:
|
|
93
|
+
event['args'] = printable(password)
|
|
94
|
+
command += ' ' + event['args']
|
|
95
|
+
else:
|
|
96
|
+
event['args'] = ''
|
|
97
|
+
message = 'Command "{}" from {}:{}.'.format(command, ip, port)
|
|
98
|
+
elif operation == 'disconnect':
|
|
99
|
+
message = '{}:{} disconnected. Reason: {}'.format(ip, port, username)
|
|
100
|
+
else:
|
|
101
|
+
command = printable(username)
|
|
102
|
+
event['command'] = command
|
|
103
|
+
if password:
|
|
104
|
+
event['args'] = printable(password)
|
|
105
|
+
command += ' ' + event['args']
|
|
106
|
+
message = 'Unknown operation "{}" from {}:{}.'.format(command, ip, port)
|
|
107
|
+
msg(message)
|
|
108
|
+
write_event(event, self.cfg)
|
|
109
|
+
|
|
110
|
+
def send_line(self, line):
|
|
111
|
+
self.transport.write(encode(line + '\r\n'))
|
|
112
|
+
|
|
113
|
+
def process_command(self, command, args):
|
|
114
|
+
needs_auth = [
|
|
115
|
+
'PASV', 'CWD', 'XCWD', 'CDUP', 'XCUP', 'SMNT',
|
|
116
|
+
'EPRT', 'EPSV', 'ALLO', 'RNFR', 'RNTO', 'DELE', 'MDTM',
|
|
117
|
+
'RMD', 'XRMD', 'MKD', 'XMKD', 'PWD', 'XPWD', 'SIZE',
|
|
118
|
+
'STRU', 'MODE', 'RETR', 'STOR', 'STOU', 'APPE', 'SITE',
|
|
119
|
+
'REST', 'ABOR', 'LIST', 'NLST', 'STAT', 'MLSD', 'MLST'
|
|
120
|
+
]
|
|
121
|
+
help_msg = (
|
|
122
|
+
"214-The following commands are recognized (* =>'s unimplemented):\r\n"
|
|
123
|
+
' CWD XCWD CDUP XCUP SMNT* QUIT PORT PASV\r\n'
|
|
124
|
+
' EPRT EPSV ALLO* RNFR RNTO DELE MDTM RMD\r\n'
|
|
125
|
+
' XRMD MKD XMKD PWD XPWD SIZE SYST HELP\r\n'
|
|
126
|
+
' NOOP FEAT OPTS AUTH CCC CONF* ENC* MIC*\r\n'
|
|
127
|
+
' PBSZ* PROT* TYPE STRU MODE RETR STOR STOU\r\n'
|
|
128
|
+
' APPE REST ABOR USER PASS ACCT* REIN* LIST\r\n'
|
|
129
|
+
' NLST STAT SITE MLSD MLST\r\n'
|
|
130
|
+
'214 Direct comments to root@0.0.0.0'
|
|
131
|
+
)
|
|
132
|
+
feat_msg = (
|
|
133
|
+
'211-Features:\r\n'
|
|
134
|
+
' MDTM\r\n'
|
|
135
|
+
' MFMT\r\n'
|
|
136
|
+
' TVFS\r\n'
|
|
137
|
+
' UTF8\r\n'
|
|
138
|
+
' MFF modify;UNIX.group;UNIX.mode;\r\n'
|
|
139
|
+
' MLST modify*;perm*;size*;type*;unique*;UNIX.group*;UNIX.mode*;UNIX.owner*;\r\n'
|
|
140
|
+
' LANG en-US*\r\n'
|
|
141
|
+
' REST STREAM\r\n'
|
|
142
|
+
' SIZE\r\n'
|
|
143
|
+
'211 End'
|
|
144
|
+
)
|
|
145
|
+
if command == 'USER':
|
|
146
|
+
self.userReceived = True
|
|
147
|
+
self.user = args
|
|
148
|
+
self.send_line('331 Password required for {}.'.format(args))
|
|
149
|
+
elif command == 'PASS' and self.userReceived:
|
|
150
|
+
self.report_event('login', self.user, args)
|
|
151
|
+
self.send_line('530 Login incorrect.')
|
|
152
|
+
self.userReceived = False
|
|
153
|
+
elif command == 'OPTS':
|
|
154
|
+
if args:
|
|
155
|
+
self.send_line('200 UTF8 set to on')
|
|
156
|
+
else:
|
|
157
|
+
self.send_line('501 Invalid number of arguments')
|
|
158
|
+
elif command == 'NOOP':
|
|
159
|
+
self.send_line('200 NOOP command successful')
|
|
160
|
+
elif command == 'SYST':
|
|
161
|
+
self.send_line('215 UNIX Type: L8')
|
|
162
|
+
elif command == 'TYPE':
|
|
163
|
+
if args:
|
|
164
|
+
arg = args.split(None, 1)[0]
|
|
165
|
+
if arg.lower() in ['a', 'i', 'l']:
|
|
166
|
+
self.send_line('200 Type set to {}'.format(arg.upper()))
|
|
167
|
+
else:
|
|
168
|
+
self.send_line("500 'TYPE {}' not understood".format(arg))
|
|
169
|
+
else:
|
|
170
|
+
self.send_line("500 'TYPE' not understood")
|
|
171
|
+
elif command == 'HOST':
|
|
172
|
+
if args:
|
|
173
|
+
arg = args.split(None, 1)[0]
|
|
174
|
+
self.send_line('504 {}: Unknown hostname provided'.format(arg))
|
|
175
|
+
else:
|
|
176
|
+
self.send_line("500 'HOST' not understood")
|
|
177
|
+
elif command == 'AUTH':
|
|
178
|
+
if len(args) == 0:
|
|
179
|
+
self.send_line('504 AUTH requires at least one argument')
|
|
180
|
+
elif args.upper().strip() not in ['TLS', 'TLS-C', 'SSL', 'TLS-P']:
|
|
181
|
+
self.send_line('500 AUTH not understood')
|
|
182
|
+
elif self.is_fttps:
|
|
183
|
+
self.send_line('200 User is already authenticated.')
|
|
184
|
+
elif self.cfg['factory_options'] is not None:
|
|
185
|
+
self.send_line('234 AUTH TLS successful')
|
|
186
|
+
self.transport.startTLS(self.cfg['factory_options'])
|
|
187
|
+
self.is_fttps = True
|
|
188
|
+
else:
|
|
189
|
+
self.send_line('500 AUTH not understood')
|
|
190
|
+
elif command == 'CCC':
|
|
191
|
+
if not self.is_fttps:
|
|
192
|
+
self.send_line('533 Command channel is alredy cleared')
|
|
193
|
+
else:
|
|
194
|
+
self.send_line('200 Clear Command Channel OK')
|
|
195
|
+
self.transport.stopTLS()
|
|
196
|
+
self.is_fttps = False
|
|
197
|
+
elif command == 'HELP':
|
|
198
|
+
if args:
|
|
199
|
+
self.process_help(args)
|
|
200
|
+
else:
|
|
201
|
+
self.send_line(help_msg)
|
|
202
|
+
elif command == 'FEAT':
|
|
203
|
+
self.send_line(feat_msg)
|
|
204
|
+
elif command == 'QUIT':
|
|
205
|
+
self.send_line('221 Goodbye.')
|
|
206
|
+
self.transport.loseConnection()
|
|
207
|
+
elif command in needs_auth:
|
|
208
|
+
self.send_line('530 Please login with USER and PASS')
|
|
209
|
+
else:
|
|
210
|
+
self.send_line('500 {} not understood'.format(command))
|
|
211
|
+
|
|
212
|
+
def process_help(self, args):
|
|
213
|
+
required_path = [
|
|
214
|
+
'CWD', 'XCWD', 'RNFR', 'RNTO', 'DELE', 'MDTM', 'STOR',
|
|
215
|
+
'RMD', 'XRMD', 'MKD', 'XMKD', 'SIZE', 'RETR', 'APPE'
|
|
216
|
+
]
|
|
217
|
+
optional_path = [
|
|
218
|
+
'LIST', 'STAT', 'MLSD', 'MLST'
|
|
219
|
+
]
|
|
220
|
+
help_texts = {
|
|
221
|
+
'CDUP': '(up one directory)',
|
|
222
|
+
'XCUP': '(up one directory)',
|
|
223
|
+
'SMNT': 'is not implemented',
|
|
224
|
+
'QUIT': '(close control connection)',
|
|
225
|
+
'PORT': '<sp> h1,h2,h3,h4,p1,p2',
|
|
226
|
+
'PASV': '(returns address/port)',
|
|
227
|
+
'EPRT': '<sp> |proto|addr|port|',
|
|
228
|
+
'EPSV': '(returns port |||port|)',
|
|
229
|
+
'ALLO': 'is not implemented (ignored)',
|
|
230
|
+
'PWD': '(returns current working directory)',
|
|
231
|
+
'XPWD': '(returns current working directory)',
|
|
232
|
+
'SYST': '(returns system type)',
|
|
233
|
+
'HELP': '[<sp> command]',
|
|
234
|
+
'NOOP': '(no operation)',
|
|
235
|
+
'FEAT': '(returns feature list)',
|
|
236
|
+
'OPTS': '<sp> command [<sp> options]',
|
|
237
|
+
'AUTH': '<sp> base64-data',
|
|
238
|
+
'CCC': '(clears protection level)',
|
|
239
|
+
'CONF': '<sp> base64-data',
|
|
240
|
+
'ENC': '<sp> base64-data',
|
|
241
|
+
'MIC': '<sp> base64-data',
|
|
242
|
+
'PBSZ': '<sp> protection buffer size',
|
|
243
|
+
'PROT': '<sp> protection code',
|
|
244
|
+
'TYPE': '<sp> type-code (A, I, L 7, L 8)',
|
|
245
|
+
'STRU': 'is not implemented (always F)',
|
|
246
|
+
'MODE': 'is not implemented (always S)',
|
|
247
|
+
'STOU': '(store unique filename)',
|
|
248
|
+
'REST': '<sp> byte-count',
|
|
249
|
+
'ABOR': '(abort current operation)',
|
|
250
|
+
'USER': '<sp> username',
|
|
251
|
+
'PASS': '<sp> password',
|
|
252
|
+
'NLST': '[<sp> (pathname)]',
|
|
253
|
+
'ACCT': 'is not implemented',
|
|
254
|
+
'REIN': 'is not implemented',
|
|
255
|
+
}
|
|
256
|
+
command = args.split(None, 1)[0].upper()
|
|
257
|
+
if command in required_path:
|
|
258
|
+
self.send_line('214 Syntax: {} <sp> pathname'.format(command))
|
|
259
|
+
elif command in optional_path:
|
|
260
|
+
self.send_line('214 Syntax: {} [<sp> pathname]'.format(command))
|
|
261
|
+
elif command in help_texts:
|
|
262
|
+
self.send_line('214 Syntax: {} {}'.format(command, help_texts[command]))
|
|
263
|
+
elif command == 'SITE':
|
|
264
|
+
self.send_line('214-HELP\r\n CHGRP\r\n214 CHMOD')
|
|
265
|
+
else:
|
|
266
|
+
self.send_line("502 Unknown command '{}'".format(command))
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class MyFTPFactory(ServerFactory):
|
|
270
|
+
def __init__(self, cfg):
|
|
271
|
+
cfg['factory_options'] = None
|
|
272
|
+
if cfg['cert']:
|
|
273
|
+
cert_data = ''
|
|
274
|
+
try:
|
|
275
|
+
with open(cfg['cert']) as f:
|
|
276
|
+
cert_data += f.read()
|
|
277
|
+
except OSError:
|
|
278
|
+
msg('Could not read the file "{}".'.format(cfg['cert']))
|
|
279
|
+
if cert_data:
|
|
280
|
+
cfg['factory_options'] = PrivateCertificate.loadPEM(cert_data).options()
|
|
281
|
+
self.cfg = cfg
|
|
282
|
+
|
|
283
|
+
def buildProtocol(self, addr):
|
|
284
|
+
return MyFTPServer(self.cfg)
|
core/tools.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from ipaddress import ip_address, ip_network
|
|
4
|
+
from os import makedirs, path
|
|
5
|
+
from string import printable as p
|
|
6
|
+
from socket import socket, AF_INET, SOCK_DGRAM
|
|
7
|
+
from sys import version_info
|
|
8
|
+
|
|
9
|
+
from core.config import CONFIG
|
|
10
|
+
|
|
11
|
+
from pytz import timezone
|
|
12
|
+
|
|
13
|
+
from twisted.python.log import msg
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
from urllib.request import urlopen
|
|
17
|
+
except ImportError:
|
|
18
|
+
from urllib import urlopen
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if version_info[0] >= 3:
|
|
22
|
+
def decode(x):
|
|
23
|
+
return x.decode('utf-8', errors='ignore')
|
|
24
|
+
def encode(x):
|
|
25
|
+
return x.encode()
|
|
26
|
+
def ord(x):
|
|
27
|
+
return x
|
|
28
|
+
def to_bytes(x):
|
|
29
|
+
return bytes(x, 'ascii')
|
|
30
|
+
def to_int(x):
|
|
31
|
+
return x
|
|
32
|
+
def unicode(x):
|
|
33
|
+
return x
|
|
34
|
+
else:
|
|
35
|
+
def decode(x):
|
|
36
|
+
return x
|
|
37
|
+
def encode(x):
|
|
38
|
+
return x
|
|
39
|
+
def to_bytes(x):
|
|
40
|
+
return bytes(x)
|
|
41
|
+
def to_int(x):
|
|
42
|
+
return ord(x)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def mkdir(dir_path):
|
|
46
|
+
if not dir_path:
|
|
47
|
+
return
|
|
48
|
+
if path.exists(dir_path) and path.isdir(dir_path):
|
|
49
|
+
return
|
|
50
|
+
makedirs(dir_path)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def import_plugins(cfg):
|
|
54
|
+
# Load output modules (inspired by the Cowrie honeypot)
|
|
55
|
+
msg('Loading the plugins...')
|
|
56
|
+
output_plugins = []
|
|
57
|
+
general_options = cfg
|
|
58
|
+
for x in CONFIG.sections():
|
|
59
|
+
if not x.startswith('output_'):
|
|
60
|
+
continue
|
|
61
|
+
if CONFIG.getboolean(x, 'enabled') is False:
|
|
62
|
+
continue
|
|
63
|
+
engine = x.split('_')[1]
|
|
64
|
+
try:
|
|
65
|
+
output = __import__('output_plugins.{}'.format(engine),
|
|
66
|
+
globals(), locals(), ['output'], 0).Output(general_options)
|
|
67
|
+
output_plugins.append(output)
|
|
68
|
+
msg('Loaded output engine: {}'.format(engine))
|
|
69
|
+
except ImportError as e:
|
|
70
|
+
msg('Failed to load output engine: {} due to ImportError: {}'.format(engine, e))
|
|
71
|
+
except Exception as e:
|
|
72
|
+
msg('Failed to load output engine: {} {}'.format(engine, e))
|
|
73
|
+
return output_plugins
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def stop_plugins(cfg):
|
|
77
|
+
msg('Stoping the plugins...')
|
|
78
|
+
for plugin in cfg['output_plugins']:
|
|
79
|
+
try:
|
|
80
|
+
plugin.stop()
|
|
81
|
+
except Exception as e:
|
|
82
|
+
msg(e)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_public_ip(ip_reporter):
|
|
87
|
+
try:
|
|
88
|
+
if version_info[0] < 3:
|
|
89
|
+
return urlopen(ip_reporter).read().decode('latin1', errors='replace').encode('utf-8')
|
|
90
|
+
else:
|
|
91
|
+
return decode(urlopen(ip_reporter).read())
|
|
92
|
+
except:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def get_local_ip():
|
|
97
|
+
s = socket(AF_INET, SOCK_DGRAM)
|
|
98
|
+
try:
|
|
99
|
+
s.connect(('10.255.255.255', 1))
|
|
100
|
+
ip = s.getsockname()[0]
|
|
101
|
+
except:
|
|
102
|
+
ip = '127.0.0.1'
|
|
103
|
+
finally:
|
|
104
|
+
s.close()
|
|
105
|
+
return ip
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_utc_time(unix_time):
|
|
109
|
+
return datetime.fromtimestamp(unix_time, tz=timezone('UTC')).isoformat() + 'Z'
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def printable(x):
|
|
113
|
+
x = encode(x)
|
|
114
|
+
if all(c in to_bytes(p) for c in x):
|
|
115
|
+
return decode(x)
|
|
116
|
+
else:
|
|
117
|
+
return ''.join('\\x{:02X}'.format(to_int(c)) for c in x)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def write_event(event, cfg):
|
|
121
|
+
ip = event['src_ip']
|
|
122
|
+
for network in cfg['blacklist']:
|
|
123
|
+
if ip_address(unicode(ip)) in ip_network(unicode(network)):
|
|
124
|
+
return
|
|
125
|
+
output_plugins = cfg['output_plugins']
|
|
126
|
+
for plugin in output_plugins:
|
|
127
|
+
try:
|
|
128
|
+
plugin.write(event)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
msg(e)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def geolocate(remote_ip, reader_city, reader_asn):
|
|
135
|
+
try:
|
|
136
|
+
response_city = reader_city.city(remote_ip)
|
|
137
|
+
city = response_city.city.name
|
|
138
|
+
if city is None:
|
|
139
|
+
city = ''
|
|
140
|
+
else:
|
|
141
|
+
city = decode(city.encode('utf-8'))
|
|
142
|
+
country = response_city.country.name
|
|
143
|
+
if country is None:
|
|
144
|
+
country = ''
|
|
145
|
+
country_code = ''
|
|
146
|
+
else:
|
|
147
|
+
country = decode(country.encode('utf-8'))
|
|
148
|
+
country_code = decode(response_city.country.iso_code.encode('utf-8'))
|
|
149
|
+
except Exception as e:
|
|
150
|
+
msg(e)
|
|
151
|
+
city = ''
|
|
152
|
+
country = ''
|
|
153
|
+
country_code = ''
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
response_asn = reader_asn.asn(remote_ip)
|
|
157
|
+
if response_asn.autonomous_system_organization is None:
|
|
158
|
+
org = ''
|
|
159
|
+
else:
|
|
160
|
+
org = decode(response_asn.autonomous_system_organization.encode('utf-8'))
|
|
161
|
+
|
|
162
|
+
if response_asn.autonomous_system_number is not None:
|
|
163
|
+
asn_num = response_asn.autonomous_system_number
|
|
164
|
+
else:
|
|
165
|
+
asn_num = 0
|
|
166
|
+
except Exception as e:
|
|
167
|
+
msg(e)
|
|
168
|
+
org = ''
|
|
169
|
+
asn_num = 0
|
|
170
|
+
return country, country_code, city, org, asn_num
|