diff --git a/docker/ciscoasa/Dockerfile b/docker/ciscoasa/Dockerfile index 21d455dd..b03136fb 100644 --- a/docker/ciscoasa/Dockerfile +++ b/docker/ciscoasa/Dockerfile @@ -1,5 +1,8 @@ FROM alpine +# Include dist +ADD dist/ /root/dist/ + # Setup env and apt RUN apk -U upgrade && \ apk add bash \ @@ -19,7 +22,8 @@ RUN apk -U upgrade && \ cd /opt/ && \ git clone https://github.com/cymmetria/ciscoasa_honeypot && \ cd ciscoasa_honeypot && \ - pip3 install -r requirements.txt && \ + pip3 install --no-cache-dir -r requirements.txt && \ + cp /root/dist/asa_server.py /opt/ciscoasa_honeypot && \ chown -R ciscoasa:ciscoasa /opt/ciscoasa_honeypot && \ # Clean up @@ -33,6 +37,7 @@ RUN apk -U upgrade && \ python3 # Start elasticsearch-head -WORKDIR /opt/ciscoasa_honeypot +WORKDIR /tmp/ciscoasa/ USER ciscoasa:ciscoasa -CMD exec python3 asa_server.py --enable_ssl --verbose >> /var/log/ciscoasa/ciscoasa.log 2>&1 +CMD cp -R /opt/ciscoasa_honeypot/* /tmp/ciscoasa && exec python3 asa_server.py --enable_ssl --verbose >> /var/log/ciscoasa/ciscoasa.log 2>&1 +#CMD cp -R /opt/ciscoasa_honeypot/* /tmp/ciscoasa && exec python3 asa_server.py --enable_ssl >> /var/log/ciscoasa/ciscoasa.log 2>&1 diff --git a/docker/ciscoasa/dist/asa_server.py b/docker/ciscoasa/dist/asa_server.py new file mode 100644 index 00000000..d02803af --- /dev/null +++ b/docker/ciscoasa/dist/asa_server.py @@ -0,0 +1,253 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os +import datetime +import socket +import logging +logging.basicConfig(format='%(message)s') +import threading +from io import BytesIO +from xml.etree import ElementTree +from http.server import HTTPServer +from socketserver import ThreadingMixIn +from http.server import SimpleHTTPRequestHandler +import ike_server + + +class NonBlockingHTTPServer(ThreadingMixIn, HTTPServer): + pass + + +def header_split(h): + return [list(map(str.strip, l.split(': ', 1))) for l in h.strip().splitlines()] + + +class WebLogicHandler(SimpleHTTPRequestHandler): + logger = None + + protocol_version = "HTTP/1.1" + + EXPLOIT_STRING = b"host-scan-reply" + RESPONSE = b""" + +9.0(1) +VPN Server could not parse request. +""" + + basepath = os.path.dirname(os.path.abspath(__file__)) + + alert_function = None + + def setup(self): + SimpleHTTPRequestHandler.setup(self) + self.request.settimeout(3) + + def send_header(self, keyword, value): + if keyword.lower() == 'server': + return + SimpleHTTPRequestHandler.send_header(self, keyword, value) + + def send_head(self): + # send_head will return a file object that do_HEAD/GET will use + # do_GET/HEAD are already implemented by SimpleHTTPRequestHandler + filename = os.path.basename(self.path.rstrip('/').split('?', 1)[0]) + + if self.path == '/': + self.send_response(200) + for k, v in header_split(""" + Content-Type: text/html + Cache-Control: no-cache + Pragma: no-cache + Set-Cookie: tg=; expires=Thu, 01 Jan 1970 22:00:00 GMT; path=/; secure + Set-Cookie: webvpn=; expires=Thu, 01 Jan 1970 22:00:00 GMT; path=/; secure + Set-Cookie: webvpnc=; expires=Thu, 01 Jan 1970 22:00:00 GMT; path=/; secure + Set-Cookie: webvpn_portal=; expires=Thu, 01 Jan 1970 22:00:00 GMT; path=/; secure + Set-Cookie: webvpnSharePoint=; expires=Thu, 01 Jan 1970 22:00:00 GMT; path=/; secure + Set-Cookie: webvpnlogin=1; path=/; secure + Set-Cookie: sdesktop=; expires=Thu, 01 Jan 1970 22:00:00 GMT; path=/; secure + """): + self.send_header(k, v) + self.end_headers() + return BytesIO(b'\n') + elif filename == 'asa': # don't allow dir listing + return self.send_file('wrong_url.html', 403) + else: + return self.send_file(filename) + + def redirect(self, loc): + self.send_response(302) + for k, v in header_split(""" + Content-Type: text/html + Content-Length: 0 + Cache-Control: no-cache + Pragma: no-cache + Location: %s + Set-Cookie: tg=; expires=Thu, 01 Jan 1970 22:00:00 GMT; path=/; secure + """ % (loc,)): + self.send_header(k, v) + self.end_headers() + + def do_GET(self): + if self.path == '/+CSCOE+/logon.html': + self.redirect('/+CSCOE+/logon.html?fcadbadd=1') + return + elif self.path.startswith('/+CSCOE+/logon.html?') and 'reason=1' in self.path: + self.wfile.write(self.send_file('logon_failure').getvalue()) + return + SimpleHTTPRequestHandler.do_GET(self) + + def do_POST(self): + data_len = int(self.headers.get('Content-length', 0)) + data = self.rfile.read(data_len) if data_len else b'' + body = self.RESPONSE + if self.EXPLOIT_STRING in data: + xml = ElementTree.fromstring(data) + payloads = [] + for x in xml.iter('host-scan-reply'): + payloads.append(x.text) + + self.alert_function(self.client_address[0], self.client_address[1], payloads) + + elif self.path == '/': + self.redirect('/+webvpn+/index.html') + return + elif self.path == '/+CSCOE+/logon.html': + self.redirect('/+CSCOE+/logon.html?fcadbadd=1') + return + elif self.path.split('?', 1)[0] == '/+webvpn+/index.html': + with open(os.path.join(self.basepath, 'asa', "logon_redir.html"), 'rb') as fh: + body = fh.read() + + self.send_response(200) + self.send_header('Content-Length', int(len(body))) + self.send_header('Content-Type', 'text/html; charset=UTF-8') + self.end_headers() + self.wfile.write(body) + return + + def send_file(self, filename, status_code=200, headers=[]): + try: + with open(os.path.join(self.basepath, 'asa', filename), 'rb') as fh: + body = fh.read() + self.send_response(status_code) + for k, v in headers: + self.send_header(k, v) + if status_code == 200: + for k, v in header_split(""" + Cache-Control: max-age=0 + Set-Cookie: webvpn=; expires=Thu, 01 Jan 1970 22:00:00 GMT; path=/; secure + Set-Cookie: webvpnc=; expires=Thu, 01 Jan 1970 22:00:00 GMT; path=/; secure + Set-Cookie: webvpnlogin=1; secure + X-Transcend-Version: 1 + """): + self.send_header(k, v) + self.send_header('Content-Length', int(len(body))) + self.send_header('Content-Type', 'text/html') + self.end_headers() + return BytesIO(body) + except IOError: + return self.send_file('wrong_url.html', 404) + + def log_message(self, format, *args): + self.logger.debug("{'timestamp': '%s', 'src_ip': '%s', 'payload_printable': '%s'}" % + (datetime.datetime.utcnow().isoformat(), + self.client_address[0], + format % args)) + + def handle_one_request(self): + """Handle a single HTTP request. + Overriden to not send 501 errors + """ + self.close_connection = True + try: + self.raw_requestline = self.rfile.readline(65537) + if len(self.raw_requestline) > 65536: + self.requestline = '' + self.request_version = '' + self.command = '' + self.close_connection = 1 + return + if not self.raw_requestline: + self.close_connection = 1 + return + if not self.parse_request(): + # An error code has been sent, just exit + return + mname = 'do_' + self.command + if not hasattr(self, mname): + self.log_request() + self.close_connection = True + return + method = getattr(self, mname) + method() + self.wfile.flush() # actually send the response if not already done. + except socket.timeout as e: + # a read or a write timed out. Discard this connection + self.log_error("Request timed out: %r", e) + self.close_connection = 1 + return + + +if __name__ == '__main__': + import click + + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger() + logger.info('info') + + @click.command() + @click.option('-h', '--host', default='0.0.0.0', help='Host to listen') + @click.option('-p', '--port', default=8443, help='Port to listen', type=click.INT) + @click.option('-i', '--ike-port', default=5000, help='Port to listen for IKE', type=click.INT) + @click.option('-s', '--enable_ssl', default=False, help='Enable SSL', is_flag=True) + @click.option('-c', '--cert', default=None, help='Certificate File Path (will generate self signed ' + 'cert if not supplied)') + @click.option('-v', '--verbose', default=False, help='Verbose logging', is_flag=True) + def start(host, port, ike_port, enable_ssl, cert, verbose): + """ + A low interaction honeypot for the Cisco ASA component capable of detecting CVE-2018-0101, + a DoS and remote code execution vulnerability + """ + def alert(cls, host, port, payloads): + logger.critical({ + 'timestamp': datetime.datetime.utcnow().isoformat(), + 'src_ip': host, + 'src_port': port, + 'payload_printable': payloads, + }) + + if verbose: + logger.setLevel(logging.DEBUG) + + requestHandler = WebLogicHandler + requestHandler.alert_function = alert + requestHandler.logger = logger + + def log_date_time_string(): + """Return the current time formatted for logging.""" + now = datetime.datetime.utcnow().isoformat() + return now + + def ike(): + ike_server.start(host, ike_port, alert, logger) + t = threading.Thread(target=ike) + t.daemon = True + t.start() + + httpd = HTTPServer((host, port), requestHandler) + if enable_ssl: + import ssl + if not cert: + import gencert + cert = gencert.gencert() + httpd.socket = ssl.wrap_socket(httpd.socket, certfile=cert, server_side=True) + + logger.info('Starting server on port {:d}/tcp, use to stop'.format(port)) + try: + httpd.serve_forever() + except KeyboardInterrupt: + pass + logger.info('Stopping server.') + httpd.server_close() + + start() diff --git a/docker/ciscoasa/docker-compose.yml b/docker/ciscoasa/docker-compose.yml index a87845a7..227b1203 100644 --- a/docker/ciscoasa/docker-compose.yml +++ b/docker/ciscoasa/docker-compose.yml @@ -13,11 +13,14 @@ services: container_name: ciscoasa restart: always stop_signal: SIGINT + tmpfs: + - /tmp/ciscoasa:uid=2000,gid=2000 networks: - ciscoasa_local ports: - "5000:5000" - "8443:8443" image: "dtagdevsec/ciscoasa:1804" + read_only: true volumes: - /data/ciscoasa/log:/var/log/ciscoasa diff --git a/docker/conpot/docker-compose.yml b/docker/conpot/docker-compose.yml index 3f1b9185..2b68e5b0 100644 --- a/docker/conpot/docker-compose.yml +++ b/docker/conpot/docker-compose.yml @@ -23,7 +23,7 @@ services: - CONPOT_TEMPLATE=default - CONPOT_TMP=/tmp/conpot tmpfs: - - /tmp/conpot:exec + - /tmp/conpot:uid=2000,gid=2000 networks: - conpot_local_default ports: @@ -52,7 +52,7 @@ services: - CONPOT_TEMPLATE=IEC104 - CONPOT_TMP=/tmp/conpot tmpfs: - - /tmp/conpot:exec + - /tmp/conpot:uid=2000,gid=2000 networks: - conpot_local_IEC104 ports: @@ -76,7 +76,7 @@ services: - CONPOT_TEMPLATE=guardian_ast - CONPOT_TMP=/tmp/conpot tmpfs: - - /tmp/conpot:exec + - /tmp/conpot:uid=2000,gid=2000 networks: - conpot_local_guardian_ast ports: @@ -99,7 +99,7 @@ services: - CONPOT_TEMPLATE=ipmi - CONPOT_TMP=/tmp/conpot tmpfs: - - /tmp/conpot:exec + - /tmp/conpot:uid=2000,gid=2000 networks: - conpot_local_ipmi ports: @@ -122,7 +122,7 @@ services: - CONPOT_TEMPLATE=kamstrup_382 - CONPOT_TMP=/tmp/conpot tmpfs: - - /tmp/conpot:exec + - /tmp/conpot:uid=2000,gid=2000 networks: - conpot_local_kamstrup_382 ports: diff --git a/docker/elk/logstash/dist/logstash.conf b/docker/elk/logstash/dist/logstash.conf index 0231e426..e2488c14 100644 --- a/docker/elk/logstash/dist/logstash.conf +++ b/docker/elk/logstash/dist/logstash.conf @@ -15,6 +15,13 @@ input { type => "P0f" } +# Ciscoasa + file { + path => ["/data/ciscoasa/log/ciscoasa.log"] + codec => plain + type => "Ciscoasa" + } + # Conpot file { path => ["/data/conpot/log/*.json"] @@ -140,6 +147,19 @@ filter { } } +# Ciscoasa + if [type] == "Ciscoasa" { + kv { + remove_char_key => " '{}" + remove_char_value => "'{}" + value_split => ":" + field_split => "," + } + date { + match => [ "timestamp", "ISO8601" ] + } + } + # Conpot if [type] == "ConPot" { date { @@ -410,7 +430,7 @@ if "_grokparsefailure" in [tags] { drop {} } } # Add T-Pot hostname and external IP - if [type] == "ConPot" or [type] == "Cowrie" or [type] == "Dionaea" or [type] == "ElasticPot" or [type] == "eMobility" or [type] == "Glastopf" or [type] == "Honeytrap" or [type] == "Heralding" or [type] == "Mailoney" or [type] == "Rdpy" or [type] == "Suricata" or [type] == "Vnclowpot" { + if [type] == "Ciscoasa" or [type] == "ConPot" or [type] == "Cowrie" or [type] == "Dionaea" or [type] == "ElasticPot" or [type] == "eMobility" or [type] == "Glastopf" or [type] == "Honeytrap" or [type] == "Heralding" or [type] == "Mailoney" or [type] == "Rdpy" or [type] == "Suricata" or [type] == "Vnclowpot" { mutate { add_field => { "t-pot_ip_ext" => "${MY_EXTIP}"