diff --git a/docker/conpot/Dockerfile b/docker/conpot/Dockerfile index 69106951..7c298afd 100644 --- a/docker/conpot/Dockerfile +++ b/docker/conpot/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.7 +FROM alpine # Include dist ADD dist/ /root/dist/ @@ -6,47 +6,55 @@ ADD dist/ /root/dist/ # Setup apt RUN apk -U --no-cache add \ build-base \ - cython-dev \ file \ git \ libev \ libtool \ libcap \ - libxml2-dev \ libxslt \ - mariadb-client-libs \ + libxslt-dev \ mariadb-dev \ pkgconfig \ - python \ - python-dev \ + python3 \ + python3-dev \ py-cffi \ py-cryptography \ - py-pip \ tcpdump \ wget && \ # Setup ConPot - git clone https://github.com/mushorg/conpot /opt/conpot && \ + git clone --depth=1 https://github.com/mushorg/conpot /opt/conpot && \ cd /opt/conpot/ && \ - git reset --hard d157229e4587188ad3d3af5dddcd71200713852d && \ - git fetch origin pull/367/head:run-without-root && \ - git checkout run-without-root && \ - git checkout master && \ - git merge run-without-root && \ - cp /root/dist/requirements.txt /opt/conpot/ && \ # Patch to accept ENV for MIB path cp /root/dist/snmp_server.py /opt/conpot/conpot/protocols/snmp/ && \ - pip install --no-cache-dir -U pip setuptools && \ - pip install --no-cache-dir . && \ + # 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 && \ + sed -i 's/port="6230"/port="623"/' /opt/conpot/conpot/templates/default/ipmi/ipmi.xml && \ + sed -i 's/port="5020"/port="502"/' /opt/conpot/conpot/templates/default/modbus/modbus.xml && \ + sed -i 's/port="10201"/port="102"/' /opt/conpot/conpot/templates/default/s7comm/s7comm.xml && \ + sed -i 's/port="16100"/port="161"/' /opt/conpot/conpot/templates/default/snmp/snmp.xml && \ + sed -i 's/port="6969"/port="69"/' /opt/conpot/conpot/templates/default/tftp/tftp.xml && \ + sed -i 's/port="16100"/port="161"/' /opt/conpot/conpot/templates/IEC104/snmp/snmp.xml && \ + sed -i 's/port="6230"/port="623"/' /opt/conpot/conpot/templates/ipmi/ipmi/ipmi.xml && \ + pip3 install --no-cache-dir -U pip setuptools && \ + pip3 install --no-cache-dir . && \ cd / && \ rm -rf /opt/conpot /tmp/* /var/tmp/* && \ - setcap cap_net_bind_service=+ep /usr/bin/python2.7 && \ + setcap cap_net_bind_service=+ep /usr/bin/python3.6 && \ + # Fix kamstrup not starting error + ln -s /usr/lib/python3.6/site-packages/conpot/protocols/kamstrup/meter_protocol /usr/lib/python3.6/site-packages/conpot/protocols/kamstrup_meter && \ + ln -s /usr/lib/python3.6/site-packages/conpot/protocols/kamstrup/management_protocol /usr/lib/python3.6/site-packages/conpot/protocols/kamstrup_management && \ # Get wireshark manuf db for scapy, setup configs, user, groups mkdir -p /etc/conpot /var/log/conpot /usr/share/wireshark && \ wget https://github.com/wireshark/wireshark/raw/master/manuf -o /usr/share/wireshark/manuf && \ cp /root/dist/conpot.cfg /etc/conpot/conpot.cfg && \ - cp -R /root/dist/templates /usr/lib/python2.7/site-packages/conpot/ && \ + cp -R /root/dist/templates /usr/lib/python3.6/site-packages/conpot/ && \ addgroup -g 2000 conpot && \ adduser -S -s /bin/ash -u 2000 -D -g 2000 conpot && \ @@ -58,17 +66,16 @@ RUN apk -U --no-cache add \ git \ libev \ libtool \ - libxml2-dev \ libxslt-dev \ mariadb-dev \ pkgconfig \ - python-dev \ + python3-dev \ py-cffi \ - py-pip \ wget && \ rm -rf /root/* && \ rm -rf /var/cache/apk/* # Start conpot USER conpot:conpot -CMD exec /usr/bin/conpot --template $CONPOT_TEMPLATE --logfile $CONPOT_LOG --config $CONPOT_CONFIG +CMD exec /usr/bin/conpot --temp_dir $CONPOT_TMP --template $CONPOT_TEMPLATE --logfile $CONPOT_LOG --config $CONPOT_CONFIG +#CMD exec /usr/bin/conpot -v --temp_dir $CONPOT_TMP --template $CONPOT_TEMPLATE --logfile $CONPOT_LOG --config $CONPOT_CONFIG diff --git a/docker/conpot/dist/command_responder.py b/docker/conpot/dist/command_responder.py new file mode 100644 index 00000000..f96f7c5d --- /dev/null +++ b/docker/conpot/dist/command_responder.py @@ -0,0 +1,1123 @@ +# Copyright (C) 2013 Daniel creo Haslinger +# +# 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 time +import random + +from datetime import datetime + +from html.parser import HTMLParser +from socketserver import ThreadingMixIn + +import http.server +import http.client +import os +from lxml import etree +from conpot.helpers import str_to_bytes +import conpot.core as conpot_core +import gevent + + +logger = logging.getLogger(__name__) + + +class HTTPServer(http.server.BaseHTTPRequestHandler): + + def log(self, version, request_type, addr, request, response=None): + + session = conpot_core.get_session('http', addr[0], addr[1]) + + log_dict = {'remote': addr, + 'timestamp': datetime.utcnow(), + 'data_type': 'http', + 'dst_port': self.server.server_port, + 'data': {0: {'request': '{0} {1}: {2}'.format(version, request_type, request)}}} + + logger.info('%s %s request from %s: %s. %s', version, request_type, addr, request, session.id) + + if response: + logger.info('%s response to %s: %s. %s', version, addr, response, session.id) + log_dict['data'][0]['response'] = '{0} response: {1}'.format(version, response) + session.add_event({'request': str(request), 'response': str(response)}) + else: + session.add_event({'request': str(request)}) + + # FIXME: Proper logging + + def get_entity_headers(self, rqfilename, headers, configuration): + + xml_headers = configuration.xpath( + '//http/htdocs/node[@name="' + rqfilename + '"]/headers/*' + ) + + if xml_headers: + + # retrieve all headers assigned to this entity + for header in xml_headers: + headers.append((header.attrib['name'], header.text)) + + return headers + + def get_trigger_appendix(self, rqfilename, rqparams, configuration): + + xml_triggers = configuration.xpath( + '//http/htdocs/node[@name="' + rqfilename + '"]/triggers/*' + ) + + if xml_triggers: + paramlist = rqparams.split('&') + + # retrieve all subselect triggers assigned to this entity + for triggers in xml_triggers: + + triggerlist = triggers.text.split(';') + trigger_missed = False + + for trigger in triggerlist: + if not trigger in paramlist: + trigger_missed = True + + if not trigger_missed: + return triggers.attrib['appendix'] + + return None + + def get_entity_trailers(self, rqfilename, configuration): + + trailers = [] + xml_trailers = configuration.xpath( + '//http/htdocs/node[@name="' + rqfilename + '"]/trailers/*' + ) + + if xml_trailers: + + # retrieve all headers assigned to this entity + for trailer in xml_trailers: + trailers.append((trailer.attrib['name'], trailer.text)) + + return trailers + + def get_status_headers(self, status, headers, configuration): + + xml_headers = configuration.xpath('//http/statuscodes/status[@name="' + + str(status) + '"]/headers/*') + + if xml_headers: + + # retrieve all headers assigned to this status + for header in xml_headers: + headers.append((header.attrib['name'], header.text)) + + return headers + + def get_status_trailers(self, status, configuration): + + trailers = [] + xml_trailers = configuration.xpath( + '//http/statuscodes/status[@name="' + str(status) + '"]/trailers/*' + ) + + if xml_trailers: + + # retrieve all trailers assigned to this status + for trailer in xml_trailers: + trailers.append((trailer.attrib['name'], trailer.text)) + + return trailers + + def send_response(self, code, message=None): + """Send the response header and log the response code. + This function is overloaded to change the behaviour when + loggers and sending default headers. + """ + + # replace integrated loggers with conpot logger.. + # self.log_request(code) + + if message is None: + if code in self.responses: + message = self.responses[code][0] + else: + message = '' + + if self.request_version != 'HTTP/0.9': + msg = str_to_bytes("{} {} {}\r\n".format(self.protocol_version, code, message)) + self.wfile.write(msg) + + # the following two headers are omitted, which is why we override + # send_response() at all. We do this one on our own... + + # - self.send_header('Server', self.version_string()) + # - self.send_header('Date', self.date_time_string()) + + def substitute_template_fields(self, payload): + + # initialize parser with our payload + parser = TemplateParser(payload) + + # triggers the parser, just in case of open / incomplete tags.. + parser.close() + + # retrieve and return (substituted) payload + return parser.payload + + def load_status(self, status, requeststring, requestheaders, headers, configuration, docpath, method='GET', body=None): + """Retrieves headers and payload for a given status code. + Certain status codes can be configured to forward the + request to a remote system. If not available, generate + a minimal response""" + + # handle PROXY tag + entity_proxy = configuration.xpath('//http/statuscodes/status[@name="' + + str(status) + + '"]/proxy') + + if entity_proxy: + source = 'proxy' + target = entity_proxy[0].xpath('./text()')[0] + else: + source = 'filesystem' + + # handle TARPIT tag + entity_tarpit = configuration.xpath( + '//http/statuscodes/status[@name="' + str(status) + '"]/tarpit' + ) + + if entity_tarpit: + tarpit = self.server.config_sanitize_tarpit(entity_tarpit[0].xpath('./text()')[0]) + else: + tarpit = None + + # check if we have to delay further actions due to global or local TARPIT configuration + if tarpit is not None: + # this node has its own delay configuration + self.server.do_tarpit(tarpit) + else: + # no delay configuration for this node. check for global latency + if self.server.tarpit is not None: + # fall back to the globally configured latency + self.server.do_tarpit(self.server.tarpit) + + # If the requested resource resides on our filesystem, + # we try retrieve all metadata and the resource itself from there. + if source == 'filesystem': + + # retrieve headers from entities configuration block + headers = self.get_status_headers(status, headers, configuration) + + # retrieve headers from entities configuration block + trailers = self.get_status_trailers(status, configuration) + + # retrieve payload directly from filesystem, if possible. + # If this is not possible, return an empty, zero sized string. + try: + if not isinstance(status, int): + status = status.value + with open(os.path.join(docpath, 'statuscodes', str(int(status)) + '.status'), 'rb') as f: + payload = f.read() + + except IOError as e: + logger.exception('%s', e) + payload = '' + + # there might be template data that can be substituted within the + # payload. We only substitute data that is going to be displayed + # by the browser: + + # perform template substitution on payload + payload = self.substitute_template_fields(payload) + + # How do we transport the content? + chunked_transfer = configuration.xpath('//http/htdocs/node[@name="' + + str(status) + '"]/chunks') + + if chunked_transfer: + # Append a chunked transfer encoding header + headers.append(('Transfer-Encoding', 'chunked')) + chunks = str(chunked_transfer[0].xpath('./text()')[0]) + else: + # Calculate and append a content length header + headers.append(('Content-Length', payload.__len__())) + chunks = '0' + + return status, headers, trailers, payload, chunks + + # the requested status code is configured to forward the + # originally targeted resource to a remote system. + + elif source == 'proxy': + + # open a connection to the remote system. + # If something goes wrong, fall back to 503. + + # NOTE: we use try:except here because there is no perfect + # platform independent way to check file accessibility. + + trailers = [] + chunks = '0' + + try: + # Modify a few headers to fit our new destination and the fact + # that we're proxying while being unaware of any session foo.. + requestheaders['Host'] = target + requestheaders['Connection'] = 'close' + + remotestatus = 0 + conn = http.client.HTTPConnection(target) + conn.request(method, requeststring, body, dict(requestheaders)) + response = conn.getresponse() + + remotestatus = int(response.status) + headers = response.getheaders() # We REPLACE the headers to avoid duplicates! + payload = response.read() + + # WORKAROUND: to get around a strange httplib-behaviour when it comes + # to chunked transfer encoding, we replace the chunked-header with a + # valid Content-Length header: + + for i, header in enumerate(headers): + + if header[0].lower() == 'transfer-encoding' and header[1].lower() == 'chunked': + del headers[i] + break + + status = remotestatus + + except: + + # before falling back to 503, we check if we are ALREADY dealing with a 503 + # to prevent an infinite request handling loop... + + if status != 503: + + # we're handling another error here. + # generate a 503 response from configuration. + (status, headers, trailers, payload, chunks) = self.load_status(503, + requeststring, + self.headers, + headers, + configuration, + docpath) + + else: + + # oops, we're heading towards an infinite loop here, + # generate a minimal 503 response regardless of the configuration. + status = 503 + payload = '' + chunks = '0' + headers.append(('Content-Length', 0)) + + return status, headers, trailers, payload, chunks + + def load_entity(self, requeststring, headers, configuration, docpath): + """ + Retrieves status, headers and payload for a given entity, that + can be stored either local or on a remote system + """ + + # extract filename and GET parameters from request string + rqfilename = requeststring.partition('?')[0] + rqparams = requeststring.partition('?')[2] + + # handle ALIAS tag + entity_alias = configuration.xpath( + '//http/htdocs/node[@name="' + rqfilename + '"]/alias' + ) + if entity_alias: + rqfilename = entity_alias[0].xpath('./text()')[0] + + # handle SUBSELECT tag + rqfilename_appendix = self.get_trigger_appendix(rqfilename, rqparams, configuration) + if rqfilename_appendix: + rqfilename += '_' + rqfilename_appendix + + # handle PROXY tag + entity_proxy = configuration.xpath( + '//http/htdocs/node[@name="' + rqfilename + '"]/proxy' + ) + if entity_proxy: + source = 'proxy' + target = entity_proxy[0].xpath('./text()')[0] + else: + source = 'filesystem' + + # handle TARPIT tag + entity_tarpit = configuration.xpath( + '//http/htdocs/node[@name="' + rqfilename + '"]/tarpit' + ) + if entity_tarpit: + tarpit = self.server.config_sanitize_tarpit(entity_tarpit[0].xpath('./text()')[0]) + else: + tarpit = None + + # check if we have to delay further actions due to global or local TARPIT configuration + if tarpit is not None: + # this node has its own delay configuration + self.server.do_tarpit(tarpit) + else: + # no delay configuration for this node. check for global latency + if self.server.tarpit is not None: + # fall back to the globally configured latency + self.server.do_tarpit(self.server.tarpit) + + # If the requested resource resides on our filesystem, + # we try retrieve all metadata and the resource itself from there. + if source == 'filesystem': + + # handle STATUS tag + # ( filesystem only, since proxied requests come with their own status ) + entity_status = configuration.xpath( + '//http/htdocs/node[@name="' + rqfilename + '"]/status' + ) + if entity_status: + status = int(entity_status[0].xpath('./text()')[0]) + else: + status = 200 + + # retrieve headers from entities configuration block + headers = self.get_entity_headers(rqfilename, headers, configuration) + + # retrieve trailers from entities configuration block + trailers = self.get_entity_trailers(rqfilename, configuration) + + # retrieve payload directly from filesystem, if possible. + # If this is not possible, return an empty, zero sized string. + if os.path.isabs(rqfilename): + relrqfilename = rqfilename[1:] + else: + relrqfilename = rqfilename + + try: + with open(os.path.join(docpath, 'htdocs', relrqfilename), 'rb') as f: + payload = f.read() + + except IOError as e: + if not os.path.isdir(os.path.join(docpath, 'htdocs', relrqfilename)): + logger.error('Failed to get template content: %s', e) + payload = '' + + # there might be template data that can be substituted within the + # payload. We only substitute data that is going to be displayed + # by the browser: + + templated = False + for header in headers: + if header[0].lower() == 'content-type' and header[1].lower() == 'text/html': + templated = True + + if templated: + # perform template substitution on payload + payload = self.substitute_template_fields(payload) + + # How do we transport the content? + chunked_transfer = configuration.xpath( + '//http/htdocs/node[@name="' + rqfilename + '"]/chunks' + ) + + if chunked_transfer: + # Calculate and append a chunked transfer encoding header + headers.append(('Transfer-Encoding', 'chunked')) + chunks = str(chunked_transfer[0].xpath('./text()')[0]) + else: + # Calculate and append a content length header + headers.append(('Content-Length', payload.__len__())) + chunks = '0' + + return status, headers, trailers, payload, chunks + + # the requested resource resides on another server, + # so we act as a proxy between client and target system + + elif source == 'proxy': + + # open a connection to the remote system. + # If something goes wrong, fall back to 503 + + trailers = [] + + try: + conn = http.client.HTTPConnection(target) + conn.request("GET", requeststring) + response = conn.getresponse() + + status = int(response.status) + headers = response.getheaders() # We REPLACE the headers to avoid duplicates! + payload = response.read() + chunks = '0' + + except: + status = 503 + (status, headers, trailers, payload, chunks) = self.load_status(status, + requeststring, + self.headers, + headers, + configuration, + docpath) + + return status, headers, trailers, payload, chunks + + def send_chunked(self, chunks, payload, trailers): + """Send payload via chunked transfer encoding to the + client, followed by eventual trailers.""" + + chunk_list = chunks.split(',') + pointer = 0 + for cwidth in chunk_list: + cwidth = int(cwidth) + # send chunk length indicator + self.wfile.write(format(cwidth, 'x').upper() + "\r\n") + # send chunk payload + self.wfile.write(payload[pointer:pointer + cwidth] + "\r\n") + pointer += cwidth + + # is there another chunk that has not been configured? Send it anyway for the sake of completeness.. + if len(payload) > pointer: + # send chunk length indicator + self.wfile.write(format(len(payload) - pointer, 'x').upper() + "\r\n") + # send chunk payload + self.wfile.write(payload[pointer:] + "\r\n") + + # we're done with the payload. Send a zero chunk as EOF indicator + self.wfile.write('0'+"\r\n") + + # if there are trailing headers :-) we send them now.. + for trailer in trailers: + self.wfile.write("%s: %s\r\n" % (trailer[0], trailer[1])) + + # and finally, the closing ceremony... + self.wfile.write("\r\n") + + def send_error(self, code, message=None): + """Send and log an error reply. + This method is overloaded to make use of load_status() + to allow handling of "Unsupported Method" errors. + """ + + headers = [] + headers.extend(self.server.global_headers) + configuration = self.server.configuration + docpath = self.server.docpath + + if not hasattr(self, 'headers'): + self.headers = self.MessageClass(self.rfile, 0) + + trace_data_length = self.headers.get('content-length') + unsupported_request_data = None + + if trace_data_length: + unsupported_request_data = self.rfile.read(int(trace_data_length)) + + # there are certain situations where variables are (not yet) registered + # ( e.g. corrupted request syntax ). In this case, we set them manually. + if hasattr(self, 'path') and self.path is not None: + requeststring = self.path + else: + requeststring = '' + self.path = None + if message is not None: + logger.info(message) + + # generate the appropriate status code, header and payload + (status, headers, trailers, payload, chunks) = self.load_status(code, + requeststring.partition('?')[0], + self.headers, + headers, + configuration, + docpath) + + # send http status to client + self.send_response(status) + + # send all headers to client + for header in headers: + self.send_header(header[0], header[1]) + + self.end_headers() + + # decide upon sending content as a whole or chunked + if chunks == '0': + # send payload as a whole to the client + if type(payload) != bytes: + payload = payload.encode() + self.wfile.write(payload) + else: + # send payload in chunks to the client + self.send_chunked(chunks, payload, trailers) + + # loggers + self.log(self.request_version, self.command, self.client_address, (self.path, + self.headers._headers, + unsupported_request_data), status) + + def do_TRACE(self): + """Handle TRACE requests.""" + + # fetch configuration dependent variables from server instance + headers = [] + headers.extend(self.server.global_headers) + configuration = self.server.configuration + docpath = self.server.docpath + + # retrieve TRACE body data + # ( sticking to the HTTP protocol, there should not be any body in TRACE requests, + # an attacker could though use the body to inject data if not flushed correctly, + # which is done by accessing the data like we do now - just to be secure.. ) + + trace_data_length = self.headers.get('content-length') + trace_data = None + + if trace_data_length: + trace_data = self.rfile.read(int(trace_data_length)) + + # check configuration: are we allowed to use this method? + if self.server.disable_method_trace is True: + + # Method disabled by configuration. Fall back to 501. + status = 501 + (status, headers, trailers, payload, chunks) = self.load_status(status, + self.path, + self.headers, + headers, + configuration, + docpath) + + else: + + # Method is enabled + status = 200 + payload = '' + headers.append(('Content-Type', 'message/http')) + + # Gather all request data and return it to sender.. + for rqheader in self.headers: + payload = payload + str(rqheader) + ': ' + self.headers.get(rqheader) + "\n" + + # send initial HTTP status line to client + self.send_response(status) + + # send all headers to client + for header in headers: + self.send_header(header[0], header[1]) + + self.end_headers() + + # send payload (the actual content) to client + if type(payload) != bytes: + payload = payload.encode() + self.wfile.write(payload) + + # loggers + self.log(self.request_version, + self.command, + self.client_address, + (self.path, self.headers._headers, trace_data), + status) + + def do_HEAD(self): + """Handle HEAD requests.""" + + # fetch configuration dependent variables from server instance + headers = list() + headers.extend(self.server.global_headers) + configuration = self.server.configuration + docpath = self.server.docpath + + # retrieve HEAD body data + # ( sticking to the HTTP protocol, there should not be any body in HEAD requests, + # an attacker could though use the body to inject data if not flushed correctly, + # which is done by accessing the data like we do now - just to be secure.. ) + + head_data_length = self.headers.get('content-length') + head_data = None + + if head_data_length: + head_data = self.rfile.read(int(head_data_length)) + + # check configuration: are we allowed to use this method? + if self.server.disable_method_head is True: + + # Method disabled by configuration. Fall back to 501. + status = 501 + (status, headers, trailers, payload, chunks) = self.load_status(status, + self.path, + self.headers, + headers, + configuration, + docpath) + + else: + + # try to find a configuration item for this GET request + entity_xml = configuration.xpath( + '//http/htdocs/node[@name="' + + self.path.partition('?')[0] + '"]' + ) + + if entity_xml: + # A config item exists for this entity. Handle it.. + (status, headers, trailers, payload, chunks) = self.load_entity(self.path, + headers, + configuration, + docpath) + + else: + # No config item could be found. Fall back to a standard 404.. + status = 404 + (status, headers, trailers, payload, chunks) = self.load_status(status, + self.path, + self.headers, + headers, + configuration, + docpath) + + # send initial HTTP status line to client + self.send_response(status) + + # send all headers to client + for header in headers: + self.send_header(header[0], header[1]) + + self.end_headers() + + # loggers + self.log(self.request_version, + self.command, + self.client_address, + (self.path, self.headers._headers, head_data), + status) + + def do_OPTIONS(self): + """Handle OPTIONS requests.""" + + # fetch configuration dependent variables from server instance + headers = [] + headers.extend(self.server.global_headers) + configuration = self.server.configuration + docpath = self.server.docpath + + # retrieve OPTIONS body data + # ( sticking to the HTTP protocol, there should not be any body in HEAD requests, + # an attacker could though use the body to inject data if not flushed correctly, + # which is done by accessing the data like we do now - just to be secure.. ) + + options_data_length = self.headers.get('content-length') + options_data = None + + if options_data_length: + options_data = self.rfile.read(int(options_data_length)) + + # check configuration: are we allowed to use this method? + if self.server.disable_method_options is True: + + # Method disabled by configuration. Fall back to 501. + status = 501 + (status, headers, trailers, payload, chunks) = self.load_status(status, + self.path, + self.headers, + headers, + configuration, + docpath) + + else: + + status = 200 + payload = '' + + # Add ALLOW header to response. GET, POST and OPTIONS are static, HEAD and TRACE are dynamic + allowed_methods = 'GET' + + if self.server.disable_method_head is False: + # add head to list of allowed methods + allowed_methods += ',HEAD' + + allowed_methods += ',POST,OPTIONS' + + if self.server.disable_method_trace is False: + allowed_methods += ',TRACE' + + headers.append(('Allow', allowed_methods)) + + # Calculate and append a content length header + headers.append(('Content-Length', payload.__len__())) + + # Append CC header + headers.append(('Connection', 'close')) + + # Append CT header + headers.append(('Content-Type', 'text/html')) + + # send initial HTTP status line to client + self.send_response(status) + + # send all headers to client + for header in headers: + self.send_header(header[0], header[1]) + + self.end_headers() + + # loggers + self.log(self.request_version, + self.command, + self.client_address, + (self.path, self.headers._headers, options_data), + status) + + def do_GET(self): + """Handle GET requests""" + + # fetch configuration dependent variables from server instance + headers = [] + headers.extend(self.server.global_headers) + configuration = self.server.configuration + docpath = self.server.docpath + + # retrieve GET body data + # ( sticking to the HTTP protocol, there should not be any body in GET requests, + # an attacker could though use the body to inject data if not flushed correctly, + # which is done by accessing the data like we do now - just to be secure.. ) + + get_data_length = self.headers.get('content-length') + get_data = None + + if get_data_length: + get_data = self.rfile.read(int(get_data_length)) + + # try to find a configuration item for this GET request + logger.debug('Trying to handle GET to resource <%s>, initiated by %s', self.path, self.client_address) + entity_xml = configuration.xpath( + '//http/htdocs/node[@name="' + self.path.partition('?')[0] + '"]' + ) + + if entity_xml: + # A config item exists for this entity. Handle it.. + (status, headers, trailers, payload, chunks) = self.load_entity(self.path, + headers, + configuration, + docpath) + + else: + # No config item could be found. Fall back to a standard 404.. + status = 404 + (status, headers, trailers, payload, chunks) = self.load_status(status, + self.path, + self.headers, + headers, + configuration, + docpath, + 'GET') + + # send initial HTTP status line to client + self.send_response(status) + + # send all headers to client + for header in headers: + self.send_header(header[0], header[1]) + + self.end_headers() + + # decide upon sending content as a whole or chunked + if chunks == '0': + # send payload as a whole to the client + self.wfile.write(str_to_bytes(payload)) + else: + # send payload in chunks to the client + self.send_chunked(chunks, payload, trailers) + + # loggers + self.log(self.request_version, + self.command, + self.client_address, + (self.path, self.headers._headers, get_data), + status) + + def do_POST(self): + """Handle POST requests""" + + # fetch configuration dependent variables from server instance + headers = list() + headers.extend(self.server.global_headers) + configuration = self.server.configuration + docpath = self.server.docpath + + # retrieve POST data ( important to flush request buffers ) + post_data_length = self.headers.get('content-length') + post_data = None + + if post_data_length: + post_data = self.rfile.read(int(post_data_length)) + + # try to find a configuration item for this POST request + entity_xml = configuration.xpath( + '//http/htdocs/node[@name="' + self.path.partition('?')[0] + '"]' + ) + + if entity_xml: + # A config item exists for this entity. Handle it.. + (status, headers, trailers, payload, chunks) = self.load_entity(self.path, + headers, + configuration, + docpath) + + else: + # No config item could be found. Fall back to a standard 404.. + status = 404 + (status, headers, trailers, payload, chunks) = self.load_status(status, + self.path, + self.headers, + headers, + configuration, + docpath, + 'POST', + post_data) + + # send initial HTTP status line to client + self.send_response(status) + + # send all headers to client + for header in headers: + self.send_header(header[0], header[1]) + + self.end_headers() + + # decide upon sending content as a whole or chunked + if chunks == '0': + # send payload as a whole to the client + if type(payload) != bytes: + payload = payload.encode() + self.wfile.write(payload) + else: + # send payload in chunks to the client + self.send_chunked(chunks, payload, trailers) + + # loggers + self.log(self.request_version, + self.command, + self.client_address, + (self.path, self.headers._headers, post_data), + status) + + +class TemplateParser(HTMLParser): + def __init__(self, data): + self.databus = conpot_core.get_databus() + if type(data) == bytes: + data = data.decode() + self.data = data + HTMLParser.__init__(self) + self.payload = self.data + self.feed(self.data) + + def handle_startendtag(self, tag, attrs): + """ handles template tags provided in XHTML notation. + + Expected format: + Example: + + at the moment, the parser is space- and case-sensitive(!), + this could be improved by using REGEX for replacing the template tags + with actual values. + """ + + source = '' + key = '' + + # only parse tags that are conpot template tags ( ) + if tag == 'condata': + + # initialize original tag (needed for value replacement) + origin = '<' + tag + + for attribute in attrs: + + # extend original tag + origin = origin + ' ' + attribute[0] + '="' + attribute[1] + '"' + + # fill variables with all meta information needed to + # gather actual data from the other engines (databus, modbus, ..) + if attribute[0] == 'source': + source = attribute[1] + elif attribute[0] == 'key': + key = attribute[1] + + # finalize original tag + origin += ' />' + + # we really need a key in order to do our work.. + if key: + # deal with databus powered tags: + if source == 'databus': + self.result = self.databus.get_value(key) + self.payload = self.payload.replace(origin, str(self.result)) + + # deal with eval powered tags: + elif source == 'eval': + result = '' + # evaluate key + try: + result = eval(key) + except Exception as e: + logger.exception(e) + self.payload = self.payload.replace(origin, result) + + +class ThreadedHTTPServer(ThreadingMixIn, http.server.HTTPServer): + """Handle requests in a separate thread.""" + + +class SubHTTPServer(ThreadedHTTPServer): + """this class is necessary to allow passing custom request handler into + the RequestHandlerClass""" + daemon_threads = True + + def __init__(self, server_address, RequestHandlerClass, template, docpath): + http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass) + + self.docpath = docpath + + # default configuration + self.update_header_date = True # this preserves authenticity + self.disable_method_head = False + self.disable_method_trace = False + self.disable_method_options = False + self.tarpit = '0' + + # load the configuration from template and parse it + # for the first time in order to reduce further handling.. + self.configuration = etree.parse(template) + + xml_config = self.configuration.xpath('//http/global/config/*') + if xml_config: + + # retrieve all global configuration entities + for entity in xml_config: + + if entity.attrib['name'] == 'protocol_version': + RequestHandlerClass.protocol_version = entity.text + + elif entity.attrib['name'] == 'update_header_date': + if entity.text.lower() == 'false': + # DATE header auto update disabled by configuration + self.update_header_date = False + elif entity.text.lower() == 'true': + # DATE header auto update enabled by configuration + self.update_header_date = True + + elif entity.attrib['name'] == 'disable_method_head': + if entity.text.lower() == 'false': + # HEAD method enabled by configuration + self.disable_method_head = False + elif entity.text.lower() == 'true': + # HEAD method disabled by configuration + self.disable_method_head = True + + elif entity.attrib['name'] == 'disable_method_trace': + if entity.text.lower() == 'false': + # TRACE method enabled by configuration + self.disable_method_trace = False + elif entity.text.lower() == 'true': + # TRACE method disabled by configuration + self.disable_method_trace = True + + elif entity.attrib['name'] == 'disable_method_options': + if entity.text.lower() == 'false': + # OPTIONS method enabled by configuration + self.disable_method_options = False + elif entity.text.lower() == 'true': + # OPTIONS method disabled by configuration + self.disable_method_options = True + + elif entity.attrib['name'] == 'tarpit': + if entity.text: + self.tarpit = self.config_sanitize_tarpit(entity.text) + + # load global headers from XML + self.global_headers = [] + xml_headers = self.configuration.xpath('//http/global/headers/*') + if xml_headers: + + # retrieve all headers assigned to this status code + for header in xml_headers: + if header.attrib['name'].lower() == 'date' and self.update_header_date is True: + # All HTTP date/time stamps MUST be represented in Greenwich Mean Time (GMT), + # without exception ( RFC-2616 ) + self.global_headers.append((header.attrib['name'], + time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime()))) + else: + self.global_headers.append((header.attrib['name'], header.text)) + + 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 seperated by semicolon and returns + # either the (sanitized) value or zero. + + if value is not None: + + x, _, y = value.partition(';') + + try: + _ = float(x) + except ValueError: + # first value is invalid, ignore the whole setting. + logger.error("Invalid tarpit value: '%s'. Assuming no latency.", value) + 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 do_tarpit(self, delay): + + # sleeps the thread for $delay ( should be either 1 float to apply a static period of time to sleep, + # or 2 floats seperated by semicolon to sleep a randomized period of time determined by ( rand[x;y] ) + + lbound, _, ubound = delay.partition(";") + + if not lbound or lbound is None: + # no lower boundary found. Assume zero latency + pass + elif not ubound or ubound is None: + # no upper boundary found. Assume static latency + gevent.sleep(float(lbound)) + else: + # both boundaries found. Assume random latency between lbound and ubound + gevent.sleep(random.uniform(float(lbound), float(ubound))) + + +class CommandResponder(object): + + def __init__(self, host, port, template, docpath): + + # Create HTTP server class + self.httpd = SubHTTPServer((host, port), HTTPServer, template, docpath) + self.server_port = self.httpd.server_port + + def serve_forever(self): + self.httpd.serve_forever() + + def stop(self): + logging.info("HTTP server will shut down gracefully as soon as all connections are closed.") + self.httpd.shutdown() diff --git a/docker/conpot/dist/conpot b/docker/conpot/dist/conpot new file mode 100755 index 00000000..34832a54 --- /dev/null +++ b/docker/conpot/dist/conpot @@ -0,0 +1,459 @@ +#!/usr/bin/env python3 +# Copyright (C) 2013 Lukas Rist +# +# 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() diff --git a/docker/conpot/dist/conpot.cfg b/docker/conpot/dist/conpot.cfg index 93fe9beb..218e7b51 100644 --- a/docker/conpot/dist/conpot.cfg +++ b/docker/conpot/dist/conpot.cfg @@ -1,6 +1,10 @@ [common] sensorid = conpot +[virtual_file_system] +data_fs_url = %(CONPOT_TMP)s +fs_url = tar:///usr/lib/python3.6/site-packages/conpot/data.tar + [session] timeout = 30 diff --git a/docker/conpot/dist/snmp_server.py b/docker/conpot/dist/snmp_server.py index a9f625bd..3f93bcbe 100644 --- a/docker/conpot/dist/snmp_server.py +++ b/docker/conpot/dist/snmp_server.py @@ -21,7 +21,7 @@ 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 @@ -30,6 +30,7 @@ import conpot.core as conpot_core logger = logging.getLogger() +@conpot_protocol class SNMPServer(object): def __init__(self, template, template_directory, args): """ diff --git a/docker/conpot/docker-compose.yml b/docker/conpot/docker-compose.yml index 462f2108..622ee7fb 100644 --- a/docker/conpot/docker-compose.yml +++ b/docker/conpot/docker-compose.yml @@ -27,11 +27,13 @@ services: networks: - conpot_local_default ports: +# - "69:69" - "80:80" - "102:102" - "161:161" - "502:502" # - "623:623" + - "2121:21" - "44818:44818" - "47808:47808" image: "dtagdevsec/conpot:1804"