rdphoneypot 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 +37 -0
- core/httpclient.py +71 -0
- core/logfile.py +75 -0
- core/output.py +41 -0
- core/paths.py +54 -0
- core/protocol.py +318 -0
- core/tools.py +164 -0
- output_plugins/__init__.py +0 -0
- output_plugins/couch.py +72 -0
- output_plugins/datadog.py +71 -0
- output_plugins/discord.py +116 -0
- output_plugins/elastic.py +139 -0
- output_plugins/hpfeed.py +43 -0
- output_plugins/influx2.py +58 -0
- output_plugins/jsonlog.py +36 -0
- output_plugins/kafka.py +57 -0
- output_plugins/localsyslog.py +64 -0
- output_plugins/mongodb.py +83 -0
- output_plugins/mysql.py +253 -0
- output_plugins/nlcvapi.py +125 -0
- output_plugins/postgres.py +198 -0
- output_plugins/redisdb.py +49 -0
- output_plugins/rethinkdblog.py +46 -0
- output_plugins/slack.py +79 -0
- output_plugins/socketlog.py +40 -0
- output_plugins/sqlite.py +201 -0
- output_plugins/telegram.py +123 -0
- output_plugins/textlog.py +31 -0
- output_plugins/xmpp.py +178 -0
- rdphoneypot/__init__.py +26 -0
- rdphoneypot/cli.py +528 -0
- rdphoneypot/data/Dockerfile +57 -0
- rdphoneypot/data/docs/INSTALL.md +411 -0
- rdphoneypot/data/docs/INSTALLWIN.md +418 -0
- rdphoneypot/data/docs/PLUGINS.md +21 -0
- rdphoneypot/data/docs/TODO.md +8 -0
- rdphoneypot/data/docs/datadog/README.md +32 -0
- rdphoneypot/data/docs/discord/README.md +58 -0
- rdphoneypot/data/docs/geoipupdtask.ps1 +270 -0
- rdphoneypot/data/docs/mysql/README.md +176 -0
- rdphoneypot/data/docs/mysql/READMEWIN.md +157 -0
- rdphoneypot/data/docs/mysql/mysql.sql +72 -0
- rdphoneypot/data/docs/postgres/README.md +184 -0
- rdphoneypot/data/docs/postgres/READMEWIN.md +196 -0
- rdphoneypot/data/docs/postgres/postgres.sql +65 -0
- rdphoneypot/data/docs/slack/README.md +68 -0
- rdphoneypot/data/docs/sqlite3/README.md +131 -0
- rdphoneypot/data/docs/sqlite3/READMEWIN.md +123 -0
- rdphoneypot/data/docs/sqlite3/sqlite3.sql +61 -0
- rdphoneypot/data/docs/telegram/README.md +103 -0
- rdphoneypot/data/etc/honeypot.cfg.base +486 -0
- rdphoneypot/data/responses/1.rss +0 -0
- rdphoneypot/data/responses/2.rss +0 -0
- rdphoneypot/data/responses/3.rss +0 -0
- rdphoneypot/data/test/test.py +169 -0
- rdphoneypot/honeypot.py +272 -0
- rdphoneypot-2.0.0.dist-info/METADATA +163 -0
- rdphoneypot-2.0.0.dist-info/RECORD +99 -0
- rdphoneypot-2.0.0.dist-info/WHEEL +6 -0
- rdphoneypot-2.0.0.dist-info/entry_points.txt +2 -0
- rdphoneypot-2.0.0.dist-info/licenses/LICENSE +674 -0
- rdphoneypot-2.0.0.dist-info/top_level.txt +4 -0
- rdpy/__init__.py +0 -0
- rdpy/core/__init__.py +0 -0
- rdpy/core/error.py +105 -0
- rdpy/core/filetimes.py +105 -0
- rdpy/core/layer.py +267 -0
- rdpy/core/log.py +80 -0
- rdpy/core/rss.py +312 -0
- rdpy/core/runtime_info.py +4 -0
- rdpy/core/type.py +1137 -0
- rdpy/protocol/__init__.py +0 -0
- rdpy/protocol/rdp/__init__.py +0 -0
- rdpy/protocol/rdp/lic.py +355 -0
- rdpy/protocol/rdp/nla/__init__.py +0 -0
- rdpy/protocol/rdp/nla/cssp.py +567 -0
- rdpy/protocol/rdp/nla/md4.py +73 -0
- rdpy/protocol/rdp/nla/ntlm.py +649 -0
- rdpy/protocol/rdp/nla/sspi.py +72 -0
- rdpy/protocol/rdp/pdu/__init__.py +0 -0
- rdpy/protocol/rdp/pdu/caps.py +545 -0
- rdpy/protocol/rdp/pdu/data.py +988 -0
- rdpy/protocol/rdp/pdu/layer.py +620 -0
- rdpy/protocol/rdp/pdu/order.py +132 -0
- rdpy/protocol/rdp/rdp.py +751 -0
- rdpy/protocol/rdp/sec.py +769 -0
- rdpy/protocol/rdp/t125/__init__.py +0 -0
- rdpy/protocol/rdp/t125/ber.py +263 -0
- rdpy/protocol/rdp/t125/gcc.py +621 -0
- rdpy/protocol/rdp/t125/mcs.py +677 -0
- rdpy/protocol/rdp/t125/per.py +310 -0
- rdpy/protocol/rdp/tpkt.py +259 -0
- rdpy/protocol/rdp/x224.py +444 -0
- rdpy/security/__init__.py +0 -0
- rdpy/security/pyDes.py +852 -0
- rdpy/security/rc4.py +63 -0
- rdpy/security/rsa_wrapper.py +112 -0
- rdpy/security/x509.py +157 -0
core/__init__.py
ADDED
|
File without changes
|
core/config.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
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(interpolation=ExtendedInterpolation())
|
|
33
|
+
parser.read(cfgfile)
|
|
34
|
+
return parser
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
CONFIG = readConfigFile(('etc/honeypot.cfg.base', 'etc/honeypot.cfg', 'honeypot.cfg'))
|
core/httpclient.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import absolute_import
|
|
2
|
+
|
|
3
|
+
from io import open
|
|
4
|
+
|
|
5
|
+
from twisted.internet.ssl import Certificate, trustRootFromCertificates
|
|
6
|
+
from twisted.python.log import msg
|
|
7
|
+
from twisted.web.client import Agent, BrowserLikePolicyForHTTPS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class _LegacyWebClientContextFactory(object):
|
|
11
|
+
@staticmethod
|
|
12
|
+
def build():
|
|
13
|
+
from twisted.internet.ssl import ClientContextFactory
|
|
14
|
+
|
|
15
|
+
class WebClientContextFactory(ClientContextFactory):
|
|
16
|
+
def getContext(self, hostname, port):
|
|
17
|
+
return ClientContextFactory.getContext(self)
|
|
18
|
+
|
|
19
|
+
return WebClientContextFactory()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load_trust_root_from_pem_bundle(pem_path):
|
|
23
|
+
with open(pem_path, 'r', encoding='utf-8') as fh:
|
|
24
|
+
content = fh.read()
|
|
25
|
+
certs = []
|
|
26
|
+
end_marker = '-----END CERTIFICATE-----'
|
|
27
|
+
for chunk in content.split(end_marker):
|
|
28
|
+
chunk = chunk.strip()
|
|
29
|
+
if not chunk:
|
|
30
|
+
continue
|
|
31
|
+
pem = chunk + '\n' + end_marker + '\n'
|
|
32
|
+
certs.append(Certificate.loadPEM(pem))
|
|
33
|
+
if not certs:
|
|
34
|
+
return None
|
|
35
|
+
return trustRootFromCertificates(certs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _trust_root(ca_certs):
|
|
39
|
+
if ca_certs:
|
|
40
|
+
try:
|
|
41
|
+
return _load_trust_root_from_pem_bundle(ca_certs)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
msg('Failed to load CA bundle {}: {}'.format(ca_certs, e))
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
import certifi
|
|
48
|
+
return _load_trust_root_from_pem_bundle(certifi.where())
|
|
49
|
+
except Exception:
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def create_http_agent(reactor, pool, plugin_name, verify_tls=True, ca_certs=None):
|
|
54
|
+
"""
|
|
55
|
+
Build a Twisted Agent with sane HTTPS defaults.
|
|
56
|
+
verify_tls=True uses BrowserLikePolicyForHTTPS for cert + hostname checks.
|
|
57
|
+
verify_tls=False falls back to a legacy context factory for compatibility.
|
|
58
|
+
"""
|
|
59
|
+
if verify_tls:
|
|
60
|
+
return Agent(
|
|
61
|
+
reactor,
|
|
62
|
+
contextFactory=BrowserLikePolicyForHTTPS(trustRoot=_trust_root(ca_certs)),
|
|
63
|
+
pool=pool
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
msg('{}: TLS certificate verification is disabled.'.format(plugin_name))
|
|
67
|
+
return Agent(
|
|
68
|
+
reactor,
|
|
69
|
+
contextFactory=_LegacyWebClientContextFactory.build(),
|
|
70
|
+
pool=pool
|
|
71
|
+
)
|
core/logfile.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from sys import stdout
|
|
4
|
+
|
|
5
|
+
from pytz import timezone
|
|
6
|
+
|
|
7
|
+
from twisted.python.log import textFromEventDict, _safeFormat, FileLogObserver, startLogging
|
|
8
|
+
from twisted.python.util import untilConcludes
|
|
9
|
+
from twisted.python.logfile import DailyLogFile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HoneypotDailyLogFile(DailyLogFile):
|
|
13
|
+
"""
|
|
14
|
+
Overload original Twisted with improved date formatting
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def suffix(self, tupledate):
|
|
18
|
+
"""
|
|
19
|
+
Return the suffix given a (year, month, day) tuple or unixtime
|
|
20
|
+
"""
|
|
21
|
+
try:
|
|
22
|
+
return '{:02d}-{:02d}-{:02d}'.format(tupledate[0], tupledate[1], tupledate[2])
|
|
23
|
+
except Exception:
|
|
24
|
+
# try taking a float unixtime
|
|
25
|
+
return '_'.join(map(str, self.toDate(tupledate)))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def myFLOemit(self, eventDict):
|
|
29
|
+
"""
|
|
30
|
+
Format the given log event as text and write it to the output file.
|
|
31
|
+
|
|
32
|
+
@param eventDict: a log event
|
|
33
|
+
@type eventDict: L{dict} mapping L{str} (native string) to L{object}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
# Custom emit for FileLogObserver
|
|
37
|
+
text = textFromEventDict(eventDict)
|
|
38
|
+
if text is None:
|
|
39
|
+
return
|
|
40
|
+
timeStr = self.formatTime(eventDict['time'])
|
|
41
|
+
fmtDict = {
|
|
42
|
+
'text': text.replace('\n', '\n\t')
|
|
43
|
+
}
|
|
44
|
+
msgStr = _safeFormat('%(text)s\n', fmtDict)
|
|
45
|
+
untilConcludes(self.write, timeStr + ' ' + msgStr)
|
|
46
|
+
untilConcludes(self.flush)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def myFLOformatTime(self, when):
|
|
50
|
+
"""
|
|
51
|
+
Log time in UTC
|
|
52
|
+
|
|
53
|
+
By default it's formatted as an ISO8601-like string (ISO8601 date and
|
|
54
|
+
ISO8601 time separated by a space). It can be customized using the
|
|
55
|
+
C{timeFormat} attribute, which will be used as input for the underlying
|
|
56
|
+
L{datetime.datetime.strftime} call.
|
|
57
|
+
|
|
58
|
+
@type when: C{int}
|
|
59
|
+
@param when: POSIX (ie, UTC) timestamp.
|
|
60
|
+
|
|
61
|
+
@rtype: C{str}
|
|
62
|
+
"""
|
|
63
|
+
timeFormatString = self.timeFormat
|
|
64
|
+
if timeFormatString is None:
|
|
65
|
+
timeFormatString = '[%Y-%m-%d %H:%M:%S.%fZ]'
|
|
66
|
+
return datetime.fromtimestamp(when, tz=timezone('UTC')).strftime(timeFormatString)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def set_logger(cfg_options):
|
|
70
|
+
FileLogObserver.emit = myFLOemit
|
|
71
|
+
FileLogObserver.formatTime = myFLOformatTime
|
|
72
|
+
if cfg_options['logfile'] is None:
|
|
73
|
+
startLogging(stdout)
|
|
74
|
+
else:
|
|
75
|
+
startLogging(HoneypotDailyLogFile.fromFullPath(cfg_options['logfile']), setStdout=False)
|
core/output.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import absolute_import
|
|
3
|
+
|
|
4
|
+
from socket import gethostname
|
|
5
|
+
|
|
6
|
+
from core.config import CONFIG
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Output(object):
|
|
10
|
+
"""
|
|
11
|
+
Abstract base class intended to be inherited by output plugins.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, general_options):
|
|
15
|
+
|
|
16
|
+
self.cfg = general_options
|
|
17
|
+
|
|
18
|
+
if not 'sensor' in self.cfg:
|
|
19
|
+
self.sensor = CONFIG.get('honeypot', 'sensor_name', fallback=gethostname())
|
|
20
|
+
else:
|
|
21
|
+
self.sensor = self.cfg['sensor']
|
|
22
|
+
|
|
23
|
+
self.start()
|
|
24
|
+
|
|
25
|
+
def start(self):
|
|
26
|
+
"""
|
|
27
|
+
Abstract method to initialize output plugin
|
|
28
|
+
"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def stop(self):
|
|
32
|
+
"""
|
|
33
|
+
Abstract method to shut down output plugin
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def write(self, event):
|
|
38
|
+
"""
|
|
39
|
+
Handle a general event within the output plugin
|
|
40
|
+
"""
|
|
41
|
+
pass
|
core/paths.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
rss/ RDP scenario files
|
|
9
|
+
|
|
10
|
+
Priority for locating the working directory:
|
|
11
|
+
1. RDPHONEYPOT_WORKDIR environment variable
|
|
12
|
+
2. Current working directory
|
|
13
|
+
|
|
14
|
+
The bundled read-only defaults (etc/*.cfg.base, rss/*.rss)
|
|
15
|
+
are installed inside the `rdphoneypot` package and located via the package's
|
|
16
|
+
own __file__ attribute, which works on all Python versions without
|
|
17
|
+
requiring pkg_resources or importlib.resources.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import absolute_import
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
from os import getcwd, environ
|
|
24
|
+
from os.path import abspath, dirname, join
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_workdir():
|
|
28
|
+
"""Return the absolute path to the runtime working directory."""
|
|
29
|
+
env = environ.get('RDPHONEYPOT_WORKDIR', '').strip()
|
|
30
|
+
if env:
|
|
31
|
+
return abspath(env)
|
|
32
|
+
return getcwd()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def workdir_path(*parts):
|
|
36
|
+
"""Return an absolute path rooted at the working directory."""
|
|
37
|
+
return join(get_workdir(), *parts)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def bundled(*parts):
|
|
41
|
+
"""
|
|
42
|
+
Return the filesystem path to a file bundled inside the installed package.
|
|
43
|
+
Arguments are path components relative to the rdphoneypot/data/ directory,
|
|
44
|
+
passed as separate strings (like os.path.join) to avoid hardcoded separators.
|
|
45
|
+
|
|
46
|
+
Uses the package's own __file__ to locate the data directory, which works
|
|
47
|
+
on all Python versions (2.7+) without requiring pkg_resources or
|
|
48
|
+
importlib.resources.
|
|
49
|
+
"""
|
|
50
|
+
# rdphoneypot/data/ lives alongside this module's package (core/ is a sibling
|
|
51
|
+
# of rdphoneypot/), so we go up one level from core/ to find rdphoneypot/data/.
|
|
52
|
+
here = dirname(abspath(__file__))
|
|
53
|
+
package_dir = join(dirname(here), 'rdphoneypot')
|
|
54
|
+
return join(package_dir, 'data', *parts)
|
core/protocol.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
|
|
2
|
+
from __future__ import absolute_import
|
|
3
|
+
|
|
4
|
+
from binascii import hexlify
|
|
5
|
+
from os import urandom
|
|
6
|
+
from time import time
|
|
7
|
+
|
|
8
|
+
from core.tools import decode, getlocalip, getutctime, write_event
|
|
9
|
+
|
|
10
|
+
from rdpy.core.rss import createReader, EventType, UpdateFormat
|
|
11
|
+
from rdpy.protocol.rdp.rdp import RDPServerObserver, ServerFactory
|
|
12
|
+
|
|
13
|
+
from twisted.internet.reactor import callLater
|
|
14
|
+
from twisted.python.log import msg
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HoneyPotServer(RDPServerObserver):
|
|
18
|
+
def __init__(self, controller, rssFileSizeList, cfg):
|
|
19
|
+
"""
|
|
20
|
+
@param controller: {RDPServerController}
|
|
21
|
+
@param rssFileSizeList: {Tuple} Tuple(Tuple(width, height), rssFilePath)
|
|
22
|
+
"""
|
|
23
|
+
RDPServerObserver.__init__(self, controller)
|
|
24
|
+
self._rssFileSizeList = rssFileSizeList
|
|
25
|
+
self._dx, self._dy = 0, 0
|
|
26
|
+
self._rssFile = None
|
|
27
|
+
self.cfg = cfg
|
|
28
|
+
self._login_logged = False # guard: log credentials only once per session
|
|
29
|
+
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
# Shared login-event helper
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
def _log_login_event(self, domain, username, password, nt_hash,
|
|
35
|
+
auth_method, hostname):
|
|
36
|
+
"""
|
|
37
|
+
Write the rdphoneypot.login event. The _login_logged guard ensures
|
|
38
|
+
this fires at most once per session even when both the NLA credentials
|
|
39
|
+
hook and onReady fire for the same connection.
|
|
40
|
+
"""
|
|
41
|
+
if self._login_logged:
|
|
42
|
+
return
|
|
43
|
+
self._login_logged = True
|
|
44
|
+
|
|
45
|
+
unix_time = time()
|
|
46
|
+
cred_label = 'nt_hash' if auth_method == 'NLA' else 'password'
|
|
47
|
+
cred_value = nt_hash if auth_method == 'NLA' else password
|
|
48
|
+
event = {
|
|
49
|
+
'eventid': 'rdphoneypot.login',
|
|
50
|
+
'message': "Login from {}:{}, hostname: '{}' to domain: '{}', "
|
|
51
|
+
"username: '{}', {}: '{}'.".format(
|
|
52
|
+
self.cfg['src_addr'], self.cfg['src_port'],
|
|
53
|
+
hostname, domain, username, cred_label, cred_value),
|
|
54
|
+
'session': self.cfg['session'],
|
|
55
|
+
'timestamp': getutctime(unix_time),
|
|
56
|
+
'unixtime': unix_time,
|
|
57
|
+
'sensor': self.cfg['sensor'],
|
|
58
|
+
'src_ip': self.cfg['src_addr'],
|
|
59
|
+
'src_port': self.cfg['src_port'],
|
|
60
|
+
'dst_port': self.cfg['port'],
|
|
61
|
+
'domain': domain,
|
|
62
|
+
'username': username,
|
|
63
|
+
'password': password,
|
|
64
|
+
'nt_hash': nt_hash,
|
|
65
|
+
'auth_method': auth_method,
|
|
66
|
+
'hostname': hostname,
|
|
67
|
+
}
|
|
68
|
+
write_event(event, self.cfg)
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------
|
|
71
|
+
# NLA credentials hook
|
|
72
|
+
# ------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
def onNLACredentials(self, nla_server):
|
|
75
|
+
"""
|
|
76
|
+
Called by NLAServer immediately after the NTLMSSP_AUTHENTICATE message
|
|
77
|
+
is parsed — before we send the (dummy) pubKeyAuth reply. This fires
|
|
78
|
+
even when Windows disconnects after our dummy reply, guaranteeing that
|
|
79
|
+
credentials are always logged for NLA connections.
|
|
80
|
+
|
|
81
|
+
Hostname is empty here because the MCS layer (which carries the client
|
|
82
|
+
name) has not yet connected. The _login_logged guard prevents onReady
|
|
83
|
+
from logging again if the connection does proceed to RDP.
|
|
84
|
+
"""
|
|
85
|
+
msg("NLA credentials: domain={!r} username={!r}".format(
|
|
86
|
+
nla_server.nla_domain, nla_server.nla_username))
|
|
87
|
+
self._log_login_event(
|
|
88
|
+
domain = nla_server.nla_domain,
|
|
89
|
+
username = nla_server.nla_username,
|
|
90
|
+
password = '',
|
|
91
|
+
nt_hash = nla_server.nla_nt_hash,
|
|
92
|
+
auth_method = 'NLA',
|
|
93
|
+
hostname = '',
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# ------------------------------------------------------------------
|
|
97
|
+
# Standard RDP observer events
|
|
98
|
+
# ------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
def onReady(self):
|
|
101
|
+
"""
|
|
102
|
+
@summary: Event use to inform state of server stack
|
|
103
|
+
First time this event is called is when human client is connected
|
|
104
|
+
Second time is after color depth nego, because color depth nego
|
|
105
|
+
restart a connection sequence
|
|
106
|
+
@see: RDPServerObserver.onReady
|
|
107
|
+
"""
|
|
108
|
+
first_connect = (self._rssFile is None)
|
|
109
|
+
|
|
110
|
+
if first_connect:
|
|
111
|
+
#compute which RSS file to keep
|
|
112
|
+
width, height = self._controller.getScreen()
|
|
113
|
+
size = width * height
|
|
114
|
+
rssFilePath = sorted(
|
|
115
|
+
self._rssFileSizeList,
|
|
116
|
+
key=lambda x: abs(x[0][0] * x[0][1] - size)
|
|
117
|
+
)[0][1]
|
|
118
|
+
msg('select file ({}, {}) -> {}'.format(width, height, rssFilePath))
|
|
119
|
+
self._rssFile = createReader(rssFilePath)
|
|
120
|
+
|
|
121
|
+
domain, username, password = self._controller.getCredentials()
|
|
122
|
+
hostname = self._controller.getHostname()
|
|
123
|
+
|
|
124
|
+
nla = getattr(self._controller._x224Layer, '_nlaServer', None)
|
|
125
|
+
if nla and nla.nla_username:
|
|
126
|
+
auth_method = 'NLA'
|
|
127
|
+
nt_hash = nla.nla_nt_hash
|
|
128
|
+
password = ''
|
|
129
|
+
else:
|
|
130
|
+
auth_method = 'RDP'
|
|
131
|
+
nt_hash = ''
|
|
132
|
+
|
|
133
|
+
# Suppress logging if all credential fields are empty — this
|
|
134
|
+
# happens on Windows SSL-retry connections that follow a failed
|
|
135
|
+
# NLA attempt: the Info packet has empty domain/user/password.
|
|
136
|
+
if username or domain or password or nt_hash:
|
|
137
|
+
self._log_login_event(
|
|
138
|
+
domain = domain,
|
|
139
|
+
username = username,
|
|
140
|
+
password = password,
|
|
141
|
+
nt_hash = nt_hash,
|
|
142
|
+
auth_method = auth_method,
|
|
143
|
+
hostname = hostname,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
self.start()
|
|
147
|
+
|
|
148
|
+
def onClose(self):
|
|
149
|
+
""" HoneyPot """
|
|
150
|
+
unix_time = time()
|
|
151
|
+
duration = int(round(unix_time - self.cfg['start']))
|
|
152
|
+
event = {
|
|
153
|
+
'eventid': 'rdphoneypot.session.closed',
|
|
154
|
+
'message': 'Closed connection from {}:{} [session: {}] after {} seconds.'.format(
|
|
155
|
+
self.cfg['src_addr'], self.cfg['src_port'],
|
|
156
|
+
self.cfg['session'], duration
|
|
157
|
+
),
|
|
158
|
+
'session': self.cfg['session'],
|
|
159
|
+
'timestamp': getutctime(unix_time),
|
|
160
|
+
'unixtime': unix_time,
|
|
161
|
+
'sensor': self.cfg['sensor'],
|
|
162
|
+
'src_ip': self.cfg['src_addr'],
|
|
163
|
+
'src_port': self.cfg['src_port'],
|
|
164
|
+
'dst_port': self.cfg['port'],
|
|
165
|
+
'duration': duration,
|
|
166
|
+
}
|
|
167
|
+
write_event(event, self.cfg)
|
|
168
|
+
|
|
169
|
+
def onKeyEventScancode(self, code, isPressed, isExtended):
|
|
170
|
+
""" HoneyPot """
|
|
171
|
+
if not self.cfg['log_scancodes']:
|
|
172
|
+
return
|
|
173
|
+
unix_time = time()
|
|
174
|
+
event = {
|
|
175
|
+
'eventid': 'rdphoneypot.scancode',
|
|
176
|
+
'message': 'Scancode: {}, isPressed: {}, isExtended: {}.'.format(
|
|
177
|
+
code, isPressed, isExtended
|
|
178
|
+
),
|
|
179
|
+
'session': self.cfg['session'],
|
|
180
|
+
'timestamp': getutctime(unix_time),
|
|
181
|
+
'unixtime': unix_time,
|
|
182
|
+
'sensor': self.cfg['sensor'],
|
|
183
|
+
'src_ip': self.cfg['src_addr'],
|
|
184
|
+
'src_port': self.cfg['src_port'],
|
|
185
|
+
'dst_port': self.cfg['port'],
|
|
186
|
+
'code': code,
|
|
187
|
+
'is_pressed': isPressed,
|
|
188
|
+
'is_extended': isExtended,
|
|
189
|
+
}
|
|
190
|
+
write_event(event, self.cfg)
|
|
191
|
+
|
|
192
|
+
def onKeyEventUnicode(self, code, isPressed):
|
|
193
|
+
""" HoneyPot """
|
|
194
|
+
if not self.cfg['log_scancodes']:
|
|
195
|
+
return
|
|
196
|
+
unix_time = time()
|
|
197
|
+
event = {
|
|
198
|
+
'eventid': 'rdphoneypot.unicode',
|
|
199
|
+
'message': 'Unicode: {}, isPressed: {}.'.format(code, isPressed),
|
|
200
|
+
'session': self.cfg['session'],
|
|
201
|
+
'timestamp': getutctime(unix_time),
|
|
202
|
+
'unixtime': unix_time,
|
|
203
|
+
'sensor': self.cfg['sensor'],
|
|
204
|
+
'src_ip': self.cfg['src_addr'],
|
|
205
|
+
'src_port': self.cfg['src_port'],
|
|
206
|
+
'dst_port': self.cfg['port'],
|
|
207
|
+
'code': code,
|
|
208
|
+
'is_pressed': isPressed,
|
|
209
|
+
}
|
|
210
|
+
write_event(event, self.cfg)
|
|
211
|
+
|
|
212
|
+
def onPointerEvent(self, x, y, button, isPressed):
|
|
213
|
+
""" HoneyPot """
|
|
214
|
+
if not self.cfg['log_pointers']:
|
|
215
|
+
return
|
|
216
|
+
unix_time = time()
|
|
217
|
+
event = {
|
|
218
|
+
'eventid': 'rdphoneypot.pointer',
|
|
219
|
+
'message': 'Pointer event: X: {}, Y: {}, button: {}, isPressed: {}.'.format(
|
|
220
|
+
x, y, button, isPressed
|
|
221
|
+
),
|
|
222
|
+
'session': self.cfg['session'],
|
|
223
|
+
'timestamp': getutctime(unix_time),
|
|
224
|
+
'unixtime': unix_time,
|
|
225
|
+
'sensor': self.cfg['sensor'],
|
|
226
|
+
'src_ip': self.cfg['src_addr'],
|
|
227
|
+
'src_port': self.cfg['src_port'],
|
|
228
|
+
'dst_port': self.cfg['port'],
|
|
229
|
+
'x': x,
|
|
230
|
+
'y': y,
|
|
231
|
+
'button': button,
|
|
232
|
+
'is_pressed': isPressed,
|
|
233
|
+
}
|
|
234
|
+
write_event(event, self.cfg)
|
|
235
|
+
|
|
236
|
+
def start(self):
|
|
237
|
+
self.loopScenario(self._rssFile.nextEvent())
|
|
238
|
+
|
|
239
|
+
def loopScenario(self, nextEvent):
|
|
240
|
+
"""
|
|
241
|
+
@summary: main loop event
|
|
242
|
+
"""
|
|
243
|
+
if nextEvent.type.value == EventType.UPDATE:
|
|
244
|
+
self._controller.sendUpdate(
|
|
245
|
+
nextEvent.event.destLeft.value + self._dx,
|
|
246
|
+
nextEvent.event.destTop.value + self._dy,
|
|
247
|
+
nextEvent.event.destRight.value + self._dx,
|
|
248
|
+
nextEvent.event.destBottom.value + self._dy,
|
|
249
|
+
nextEvent.event.width.value,
|
|
250
|
+
nextEvent.event.height.value,
|
|
251
|
+
nextEvent.event.bpp.value,
|
|
252
|
+
nextEvent.event.format.value == UpdateFormat.BMP,
|
|
253
|
+
nextEvent.event.data.value)
|
|
254
|
+
|
|
255
|
+
elif nextEvent.type.value == EventType.CLOSE:
|
|
256
|
+
self._controller.close()
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
elif nextEvent.type.value == EventType.SCREEN:
|
|
260
|
+
self._controller.setColorDepth(nextEvent.event.colorDepth.value)
|
|
261
|
+
clientSize = nextEvent.event.width.value, nextEvent.event.height.value
|
|
262
|
+
serverSize = self._controller.getScreen()
|
|
263
|
+
self._dx = max(0, serverSize[0] - clientSize[0]) // 2
|
|
264
|
+
self._dy = max(0, serverSize[1] - clientSize[1]) // 2
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
e = self._rssFile.nextEvent()
|
|
268
|
+
callLater(float(e.timestamp.value) / 1000.0, # pylint: disable=no-member
|
|
269
|
+
lambda: self.loopScenario(e))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class HoneyPotServerFactory(ServerFactory):
|
|
273
|
+
"""
|
|
274
|
+
@summary: Factory on listening events
|
|
275
|
+
"""
|
|
276
|
+
def __init__(self, rssFileSizeList, cfg):
|
|
277
|
+
"""
|
|
278
|
+
@param rssFileSizeList: {Tuple} Tuple(Tuple(width, height), rssFilePath)
|
|
279
|
+
@param privateKeyFilePath: {str} file contain server private key (if none -> back to standard RDP security)
|
|
280
|
+
@param certificateFilePath: {str} file contain server certificate (if none -> back to standard RDP security)
|
|
281
|
+
"""
|
|
282
|
+
ServerFactory.__init__(self, 16, cfg['key'], cfg['cert'])
|
|
283
|
+
self._rssFileSizeList = rssFileSizeList
|
|
284
|
+
self.cfg = cfg
|
|
285
|
+
|
|
286
|
+
def buildObserver(self, controller, addr):
|
|
287
|
+
"""
|
|
288
|
+
@param controller: {RDPServerController}
|
|
289
|
+
@param addr: destination address
|
|
290
|
+
@see: ServerFactory.buildObserver
|
|
291
|
+
"""
|
|
292
|
+
self.cfg['session'] = decode(hexlify(urandom(6)))
|
|
293
|
+
self.cfg['src_addr'] = addr.host
|
|
294
|
+
self.cfg['src_port'] = addr.port
|
|
295
|
+
unix_time = time()
|
|
296
|
+
self.cfg['start'] = unix_time
|
|
297
|
+
event = {
|
|
298
|
+
'eventid': 'rdphoneypot.session.connect',
|
|
299
|
+
'message': 'Connection from {}:{} [session: {}].'.format(
|
|
300
|
+
addr.host, addr.port, self.cfg['session']),
|
|
301
|
+
'session': self.cfg['session'],
|
|
302
|
+
'timestamp': getutctime(unix_time),
|
|
303
|
+
'unixtime': unix_time,
|
|
304
|
+
'src_ip': addr.host,
|
|
305
|
+
'src_port': addr.port,
|
|
306
|
+
'dst_ip': getlocalip(),
|
|
307
|
+
'dst_port': self.cfg['port'],
|
|
308
|
+
'sensor': self.cfg['sensor'],
|
|
309
|
+
}
|
|
310
|
+
write_event(event, self.cfg)
|
|
311
|
+
|
|
312
|
+
observer = HoneyPotServer(controller, self._rssFileSizeList, self.cfg)
|
|
313
|
+
|
|
314
|
+
# Register the NLA hook so credentials are logged the moment
|
|
315
|
+
# NTLMSSP_AUTHENTICATE arrives, before the pubKeyAuth round-trip.
|
|
316
|
+
controller.setNLACredentialsHook(observer.onNLACredentials)
|
|
317
|
+
|
|
318
|
+
return observer
|