conpot cleanup

This commit is contained in:
listbot 2018-08-28 13:47:20 +00:00
parent 79bb324a4a
commit 775c7aeb95
4 changed files with 1 additions and 685 deletions

View file

@ -25,14 +25,10 @@ RUN apk -U --no-cache add \
# Setup ConPot
git clone --depth=1 https://github.com/mushorg/conpot /opt/conpot && \
cd /opt/conpot/ && \
# Patch Conpot to PR#400
git pull origin pull/400/head && \
# Patch to accept ENV for MIB path
cp /root/dist/snmp_server.py /opt/conpot/conpot/protocols/snmp/ && \
sed -i "s/tmp_mib_dir = tempfile.mkdtemp()/tmp_mib_dir = tempfile.mkdtemp(dir=os.environ['CONPOT_TMP'])/" /opt/conpot/conpot/protocols/snmp/snmp_server.py && \
# Increase logging for debug mushorg/conpot/issues/#399
cp /root/dist/command_responder.py /opt/conpot/conpot/protocols/http/ && \
# mushorg/conpot/issues/#398
#cp /root/dist/conpot /opt/conpot/bin && \
# Change template default ports if <1024
sed -i 's/port="2121"/port="21"/' /opt/conpot/conpot/templates/default/ftp/ftp.xml && \
sed -i 's/port="8800"/port="80"/' /opt/conpot/conpot/templates/default/http/http.xml && \

View file

@ -1,459 +0,0 @@
#!/usr/bin/env python3
# Copyright (C) 2013 Lukas Rist <glaslos@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import gevent.monkey; gevent.monkey.patch_all()
import logging
import os
import argparse
import sys
import pwd
import grp
import ast
import inspect
from configparser import ConfigParser, NoSectionError, NoOptionError
import gevent
from lxml import etree
import conpot
import conpot.core as conpot_core
from conpot.core.loggers.log_worker import LogWorker
from conpot.protocols.snmp.snmp_server import SNMPServer
from conpot.protocols.modbus.modbus_server import ModbusServer
from conpot.protocols.s7comm.s7_server import S7Server
from conpot.protocols.http.web_server import HTTPServer
from conpot.protocols.enip.enip_server import EnipServer
from conpot.helpers import fix_sslwrap
from conpot.protocols.kamstrup.meter_protocol.kamstrup_server import KamstrupServer
from conpot.protocols.kamstrup.management_protocol.kamstrup_management_server import KamstrupManagementServer
from conpot.protocols.bacnet.bacnet_server import BacnetServer
from conpot.protocols.ipmi.ipmi_server import IpmiServer
from conpot.protocols.guardian_ast.guardian_ast_server import GuardianASTServer
from conpot.protocols.IEC104.IEC104_server import IEC104Server
from conpot.protocols.ftp.ftp_server import FTPServer
from conpot.protocols.tftp.tftp_server import TftpServer
from conpot.emulators.proxy import Proxy
from conpot.utils import ext_ip
from conpot.utils import mac_addr
logger = logging.getLogger()
package_directory = os.path.dirname(os.path.abspath(conpot.__file__))
core_interface = conpot_core.get_interface()
def logo():
print("""
_
___ ___ ___ ___ ___| |_
| _| . | | . | . | _|
|___|___|_|_| _|___|_|
|_|
Version {0}
MushMush Foundation
""".format(conpot.__version__))
def on_unhandled_greenlet_exception(dead_greenlet):
logger.error('Stopping because %s died: %s', dead_greenlet, dead_greenlet.exception)
sys.exit(1)
def setup_logging(log_file, verbose):
if verbose:
log_level = logging.DEBUG
else:
log_level = logging.INFO
log_format = logging.Formatter('%(asctime)-15s %(message)s')
console_log = logging.StreamHandler()
console_log.setLevel(log_level)
console_log.setFormatter(log_format)
logger.setLevel(log_level)
file_log = logging.FileHandler(log_file)
file_log.setFormatter(log_format)
file_log.setLevel(log_level)
root_logger = logging.getLogger()
root_logger.addHandler(console_log)
root_logger.addHandler(file_log)
# fixme: drop privs need sudo user. Figure alternatives.
def drop_privileges(uid_name=None, gid_name=None):
if uid_name is None:
uid_name = 'nobody'
try:
wanted_user = pwd.getpwnam(uid_name)
except KeyError:
logger.exception(
'Cannot drop privileges: user "%s" does not exist.',
uid_name)
sys.exit(1)
if gid_name is None:
gid_name = grp.getgrgid(wanted_user.pw_gid).gr_name
try:
wanted_group = grp.getgrnam(gid_name)
except KeyError:
logger.exception(
'Cannot drop privileges: group "%s" does not exist.',
gid_name)
sys.exit(1)
logger.debug('Attempting to drop privileges to "%s:%s"',
wanted_user.pw_name, wanted_group.gr_name)
os.setgid(wanted_group.gr_gid)
os.setuid(wanted_user.pw_uid)
new_user = pwd.getpwuid(os.getuid())
new_group = grp.getgrgid(os.getgid())
logger.info('Privileges dropped, running as "%s:%s"',
new_user.pw_name, new_group.gr_name)
def validate_template(xml_file, xsd_file):
xml_schema = etree.parse(xsd_file)
xsd = etree.XMLSchema(xml_schema)
xml = etree.parse(xml_file)
xsd.validate(xml)
if xsd.error_log:
logger.error('Error parsing XML template: %s', xsd.error_log)
sys.exit(1)
def main():
logo()
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--template",
help="Name of one of the supplied templates, or the full path to a custom template.",
default=''
)
parser.add_argument("-f", "--force",
help="Force use testing config.",
metavar="testing.cfg",
action='store_const',
const=True,
default=False
)
parser.add_argument("-c", "--config",
help="The configuration file to use",
metavar="conpot.cfg",
)
parser.add_argument("-l", "--logfile",
help="The logfile to use",
default="conpot.log"
)
parser.add_argument("-a", "--raw_mib",
help="Path to raw MIB files."
"(will automatically get compiled by build-pysnmp-mib)",
action='append',
default=[]
)
parser.add_argument("-m", "--mibpaths",
action='append',
help="Path to compiled PySNMP MIB files."
"(must be compiled with build-pysnmp-mib)",
default=[]
)
parser.add_argument("--temp_dir",
help="Directory where all conpot vfs related files would be kept.",
default="ConpotTempFS"
)
parser.add_argument('-v', '--verbose', action='store_true', default=False, help='Logs debug messages.')
args = parser.parse_args()
setup_logging(args.logfile, args.verbose)
core_interface.config = ConfigParser(os.environ)
config = core_interface.config
if os.getuid() == 0:
if not args.force:
logger.critical("Can't start conpot with root. Please ref user docs for more info.")
sys.exit(3)
else:
logger.warning('Running conpot with root. Running conpot with root isn\'t recommended. ')
if os.getuid() == 0:
try:
# retrieve user to run as
conpot_user = config.get('daemon', 'user')
except (NoSectionError, NoOptionError):
conpot_user = None
try:
# retrieve group to run as
conpot_group = config.get('daemon', 'group')
except (NoSectionError, NoOptionError):
conpot_group = None
# FIXME: drop privs require sudo
drop_privileges(conpot_user, conpot_group)
# Loading default config if config not set.
if args.force:
args.config = os.path.join(package_directory, 'testing.cfg')
logger.warning('--force option specified. Using testing configuration')
config.read(args.config)
else:
# sanity check: see that both config and template arguments are provided - else exit
if not (args.config and args.template):
print('Invalid arguments supplied. Please check that you pass both template and config arguments before'
' running Conpot')
sys.exit(3)
try:
if not os.path.isfile(os.path.join(package_directory, args.config)):
raise FileNotFoundError('Config file not found!')
args.config = os.path.join(package_directory, args.config)
logger.info('Config file found!')
config.read(args.config)
except FileNotFoundError:
logger.exception('\nCould not find config file!\nUse -f option to try the test configuration')
sys.exit(1)
# No template specified
if not args.template:
available_templates = os.listdir(os.path.join(package_directory, 'templates'))
print("--------------------------------------------------")
print(" Available templates:")
print("--------------------------------------------------\n")
for folder in available_templates:
template_xml = os.path.join(package_directory, 'templates', folder, 'template.xml')
if os.path.isfile(template_xml):
template_unit = template_vendor = template_description = template_protocols = template_creator = 'N/A'
dom_template = etree.parse(template_xml)
template_details = dom_template.xpath('//core/template/*')
if template_details:
# retrieve all template details
for entity in template_details:
if entity.attrib['name'] == 'unit':
template_unit = entity.text
elif entity.attrib['name'] == 'vendor':
template_vendor = entity.text
elif entity.attrib['name'] == 'description':
template_description = entity.text
elif entity.attrib['name'] == 'protocols':
template_protocols = entity.text
elif entity.attrib['name'] == 'creator':
template_creator = entity.text
print(" --template {0}".format(folder))
print(" Unit: {0} - {1}".format(template_vendor, template_unit))
print(" Desc: {0}".format(template_description))
print(" Protocols: {0}".format(template_protocols))
print(" Created by: {0}\n".format(template_creator))
sys.exit(0)
# Custom template supplied
if os.path.exists(os.path.join(args.template, 'template.xml')):
root_template_directory = args.template
# Check if the template name can be in the default templates directory
elif os.path.isfile(os.path.join(package_directory, 'templates', args.template, 'template.xml')):
root_template_directory = os.path.join(package_directory, 'templates', args.template)
else:
logger.error('Template not found: %s', args.template)
sys.exit(1)
# Check if the configuration file exists..
if not os.path.isfile(args.config):
logger.error('Config not found: %s', args.config)
sys.exit(1)
logger.info('Starting Conpot using template: %s', root_template_directory)
logger.info('Starting Conpot using configuration found in: %s', args.config)
servers = list()
template_base = os.path.join(root_template_directory, 'template.xml')
if os.path.isfile(template_base):
validate_template(template_base, os.path.join(package_directory, 'tests/template_schemas/core.xsd'))
dom_base = etree.parse(template_base)
else:
logger.error('Could not access template configuration')
sys.exit(1)
session_manager = conpot_core.get_sessionManager()
session_manager.initialize_databus(template_base)
# initialize the virtual file system
fs_url = config.get('virtual_file_system', 'fs_url')
data_fs_url = config.get('virtual_file_system', 'data_fs_url')
if os.path.isdir(args.temp_dir):
temp_dir = args.temp_dir
else:
temp_dir = os.path.join(conpot.__path__[0], 'ConpotTempFS')
logger.info("Can't find the temp directory. Conpot VFS would be kept at : {}".format(temp_dir))
if not os.path.exists(temp_dir):
os.mkdir(temp_dir)
if fs_url == 'default' or data_fs_url == 'default':
if not args.force:
logger.error('Can\'t start conpot with default file system')
sys.exit(3)
else:
fs_url, data_fs_url = None, None
else:
logger.info('Serving {} as file system. File uploads will be kept at : {}'.format(fs_url, data_fs_url))
conpot_core.initialize_vfs(fs_url, data_fs_url, temp_dir)
public_ip = None
if config.getboolean('fetch_public_ip', 'enabled'):
public_ip = ext_ip.get_ext_ip(config)
if config.getboolean('change_mac_addr', 'enabled'):
if os.getuid() == 0:
logger.info('Attempting to change mac address.')
mac_addr.change_mac(config=config)
else:
logger.info('Changing mac address require sudo permissions. Skipping')
protocol_instance_mapping = (
('modbus', ModbusServer),
('s7comm', S7Server),
('kamstrup_meter', KamstrupServer),
('kamstrup_management', KamstrupManagementServer),
('http', HTTPServer),
('snmp', SNMPServer),
('bacnet', BacnetServer),
('ipmi', IpmiServer),
('guardian_ast', GuardianASTServer),
('enip', EnipServer),
('IEC104', IEC104Server),
('ftp', FTPServer),
('tftp', TftpServer)
)
# no need to fork process when we don't want to change MAC address
pid = 0
if config.getboolean('change_mac_addr', 'enabled'):
pid = gevent.fork()
if pid == 0:
for protocol in protocol_instance_mapping:
protocol_name, server_class = protocol
protocol_template = os.path.join(root_template_directory, protocol_name, '{0}.xml'.format(protocol_name))
if os.path.isfile(protocol_template):
if 'kamstrup' in protocol_name:
kamstrup_dir, protocol_name = os.path.split((protocol_name.replace('_', '/')) + '_protocol')
xsd_file = os.path.join(package_directory, 'protocols', kamstrup_dir, protocol_name, '{0}.xsd'.format(protocol_name))
else:
xsd_file = os.path.join(package_directory, 'protocols', protocol_name, '{0}.xsd'.format(protocol_name))
validate_template(protocol_template, xsd_file)
dom_protocol = etree.parse(protocol_template)
if dom_protocol.xpath('//{0}'.format(protocol_name)):
if ast.literal_eval(dom_protocol.xpath('//{0}/@enabled'.format(protocol_name))[0]):
host = dom_protocol.xpath('//{0}/@host'.format(protocol_name))[0]
# -- > Are we running on testing config?
if 'testing.cfg' in args.config:
if '127.' not in host:
if not args.force:
logger.error('To run conpot on a non local interface, please specify -f option')
sys.exit(1)
port = ast.literal_eval(dom_protocol.xpath('//{0}/@port'.format(protocol_name))[0])
server = server_class(protocol_template, root_template_directory, args)
greenlet = gevent.spawn(server.start, host, port)
greenlet.link_exception(on_unhandled_greenlet_exception)
servers.append(server)
logger.info('Found and enabled %s protocol.', (protocol[0], server))
else:
logger.info('%s available but disabled by configuration.', protocol_name)
else:
logger.debug('No %s template found. Service will remain unconfigured/stopped.', protocol_name)
log_worker = LogWorker(config, dom_base, session_manager, public_ip)
greenlet = gevent.spawn(log_worker.start)
greenlet.link_exception(on_unhandled_greenlet_exception)
# TODO: Make proxy fit into protocol_instance_mapping
template_proxy = os.path.join(root_template_directory, 'proxy', 'proxy.xml')
if os.path.isfile(template_proxy):
xsd_file = os.path.join(os.path.dirname(inspect.getfile(Proxy)), 'proxy.xsd')
validate_template(template_proxy, xsd_file)
dom_proxy = etree.parse(template_proxy)
if dom_proxy.xpath('//proxies'):
if ast.literal_eval(dom_proxy.xpath('//proxies/@enabled')[0]):
proxies = dom_proxy.xpath('//proxies/*')
for p in proxies:
name = p.attrib['name']
host = p.attrib['host']
keyfile = None
certfile = None
if 'keyfile' in p.attrib and 'certfile' in p.attrib:
keyfile = p.attrib['keyfile']
certfile = p.attrib['certfile']
# if path is absolute we assert that the cert and key is located in
# the templates ssl standard location
if not os.path.isabs(keyfile):
keyfile = os.path.join(os.path.dirname(root_template_directory), 'ssl', keyfile)
certfile = os.path.join(os.path.dirname(root_template_directory), 'ssl', certfile)
port = ast.literal_eval(p.attrib['port'])
proxy_host = p.xpath('./proxy_host/text()')[0]
proxy_port = ast.literal_eval(p.xpath('./proxy_port/text()')[0])
decoder = p.xpath('./decoder/text()')
if len(decoder) > 0:
decoder = decoder[0]
else:
decoder = None
proxy_instance = Proxy(name, proxy_host, proxy_port, decoder, keyfile, certfile)
proxy_server = proxy_instance.get_server(host, port)
servers.append(proxy_instance)
proxy_greenlet = gevent.spawn(proxy_server.start)
proxy_greenlet.link_exception(on_unhandled_greenlet_exception)
else:
logger.info('Proxy available but disabled by template.')
else:
logger.info('No proxy template found. Service will remain unconfigured/stopped.')
# Wait for the services to bind ports before forking
gevent.sleep(5)
try:
if len(servers) > 0:
gevent.wait()
except KeyboardInterrupt:
logging.info('Stopping Conpot')
for server in servers:
server.stop()
finally:
conpot_core.close_fs()
else:
# wait for the child to end
try:
os.waitpid(pid, 0)
except KeyboardInterrupt:
pass
# Revert MAC address
iface = config.get('change_mac_addr', 'iface')
mac_addr.revert_mac(iface)
if __name__ == "__main__":
fix_sslwrap()
main()

View file

@ -1,25 +0,0 @@
bacpypes==0.16.1
beautifulsoup4==4.6.0
bottle==0.12.13
cpppo==3.9.7
crc16==0.1.1
cybox==2.1.0.13
enum34==1.1.6
gevent==1.3a1
hpfeeds==1.0
jinja2==2.10
libtaxii==1.1.110
lxml==4.1.1
mixbox==1.0.2
modbus-tk==0.5.8
MySQL-python==1.2.5
natsort==5.2.0
pyghmi==1.0.38
pysmi==0.2.2
pysnmp==4.4.4
requests==2.18.4
scapy==2.4.0rc4
sphinx==1.5.5
stix==1.2.0.2
stix-validator==2.5.0
xlrd==1.1.0

View file

@ -1,196 +0,0 @@
# Copyright (C) 2013 Lukas Rist <glaslos@gmail.com>
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import logging
import tempfile
import shutil
import os
from lxml import etree
from conpot.core.protocol_wrapper import conpot_protocol
from conpot.protocols.snmp.command_responder import CommandResponder
from conpot.protocols.snmp.build_pysnmp_mib_wrapper import find_mibs, compile_mib
import conpot.core as conpot_core
logger = logging.getLogger()
@conpot_protocol
class SNMPServer(object):
def __init__(self, template, template_directory, args):
"""
:param host: hostname or ip address on which to server the snmp service (string).
:param port: listen port (integer).
:param template: path to the protocol specific xml configuration file (string).
"""
self.dom = etree.parse(template)
self.cmd_responder = None
if args.mibpaths:
self.compiled_mibs = args.mibpaths
else:
self.compiled_mibs = [os.path.join(template_directory, 'snmp', 'mibs_compiled')]
if args.raw_mib:
self.raw_mibs = args.raw_mib
else:
self.raw_mibs = [os.path.join(template_directory, 'snmp', 'mibs_raw')]
def xml_general_config(self, dom):
snmp_config = dom.xpath('//snmp/config/*')
if snmp_config:
for entity in snmp_config:
# TARPIT: individual response delays
if entity.attrib['name'].lower() == 'tarpit':
if entity.attrib['command'].lower() == 'get':
self.cmd_responder.resp_app_get.tarpit = self.config_sanitize_tarpit(entity.text)
elif entity.attrib['command'].lower() == 'set':
self.cmd_responder.resp_app_set.tarpit = self.config_sanitize_tarpit(entity.text)
elif entity.attrib['command'].lower() == 'next':
self.cmd_responder.resp_app_next.tarpit = self.config_sanitize_tarpit(entity.text)
elif entity.attrib['command'].lower() == 'bulk':
self.cmd_responder.resp_app_bulk.tarpit = self.config_sanitize_tarpit(entity.text)
# EVASION: response thresholds
if entity.attrib['name'].lower() == 'evasion':
if entity.attrib['command'].lower() == 'get':
self.cmd_responder.resp_app_get.threshold = self.config_sanitize_threshold(entity.text)
elif entity.attrib['command'].lower() == 'set':
self.cmd_responder.resp_app_set.threshold = self.config_sanitize_threshold(entity.text)
elif entity.attrib['command'].lower() == 'next':
self.cmd_responder.resp_app_next.threshold = self.config_sanitize_threshold(entity.text)
elif entity.attrib['command'].lower() == 'bulk':
self.cmd_responder.resp_app_bulk.threshold = self.config_sanitize_threshold(entity.text)
def xml_mib_config(self, dom, mibpaths, rawmibs_dirs):
try:
mibs = dom.xpath('//snmp/mibs/*')
tmp_mib_dir = tempfile.mkdtemp(dir=os.environ['CONPOT_TMP'])
mibpaths.append(tmp_mib_dir)
available_mibs = find_mibs(rawmibs_dirs)
databus = conpot_core.get_databus()
# parse mibs and oid tables
for mib in mibs:
mib_name = mib.attrib['name']
# compile the mib file if it is found and not already loaded.
if mib_name in available_mibs and not self.cmd_responder.has_mib(mib_name):
compile_mib(mib_name, tmp_mib_dir)
for symbol in mib:
symbol_name = symbol.attrib['name']
# retrieve instance from template
if 'instance' in symbol.attrib:
# convert instance to (int-)tuple
symbol_instance = symbol.attrib['instance'].split('.')
symbol_instance = tuple(map(int, symbol_instance))
else:
# use default instance (0)
symbol_instance = (0,)
# retrieve value from databus
value = databus.get_value(symbol.xpath('./value/text()')[0])
profile_map_name = symbol.xpath('./value/text()')[0]
# register this MIB instance to the command responder
self.cmd_responder.register(mib_name,
symbol_name,
symbol_instance,
value,
profile_map_name)
finally:
# cleanup compiled mib files
shutil.rmtree(tmp_mib_dir)
def config_sanitize_tarpit(self, value):
# checks tarpit value for being either a single int or float,
# or a series of two concatenated integers and/or floats separated by semicolon and returns
# either the (sanitized) value or zero.
if value is not None:
x, _, y = value.partition(';')
try:
_ = float(x)
except ValueError:
logger.error("SNMP invalid tarpit value: '%s'. Assuming no latency.", value)
# first value is invalid, ignore the whole setting.
return '0;0'
try:
_ = float(y)
# both values are fine.
return value
except ValueError:
# second value is invalid, use the first one.
return x
else:
return '0;0'
def config_sanitize_threshold(self, value):
# checks DoS thresholds for being either a single int or a series of two concatenated integers
# separated by semicolon and returns either the (sanitized) value or zero.
if value is not None:
x, _, y = value.partition(';')
try:
_ = int(x)
except ValueError:
logger.error("SNMP invalid evasion threshold: '%s'. Assuming no DoS evasion.", value)
# first value is invalid, ignore the whole setting.
return '0;0'
try:
_ = int(y)
# both values are fine.
return value
except ValueError:
# second value is invalid, use the first and ignore the second.
return str(x) + ';0'
else:
return '0;0'
def start(self, host, port):
self.cmd_responder = CommandResponder(host, port, self.compiled_mibs)
self.xml_general_config(self.dom)
self.xml_mib_config(self.dom, self.compiled_mibs, self.raw_mibs)
logger.info('SNMP server started on: %s', (host, self.get_port()))
self.cmd_responder.serve_forever()
def stop(self):
if self.cmd_responder:
self.cmd_responder.stop()
def get_port(self):
if self.cmd_responder:
return self.cmd_responder.server_port
else:
return None