diff --git a/docker/ciscoasa/Dockerfile b/docker/ciscoasa/Dockerfile index 518cdfb7..e1734141 100644 --- a/docker/ciscoasa/Dockerfile +++ b/docker/ciscoasa/Dockerfile @@ -1,7 +1,4 @@ -FROM alpine:3.19 -# -# Include dist -COPY dist/ /root/dist/ +FROM alpine:3.20 AS builder # # Install packages RUN apk --no-cache -U add build-base \ @@ -15,33 +12,22 @@ RUN apk --no-cache -U add build-base \ python3 \ python3-dev && \ # -# Setup user - addgroup -g 2000 ciscoasa && \ - adduser -S -s /bin/bash -u 2000 -D -g 2000 ciscoasa && \ -# # Get and install packages mkdir -p /opt/ && \ cd /opt/ && \ - git clone https://github.com/cymmetria/ciscoasa_honeypot && \ + git clone https://github.com/t3chn0m4g3/ciscoasa_honeypot && \ cd ciscoasa_honeypot && \ - git checkout d6e91f1aab7fe6fc01fabf2046e76b68dd6dc9e2 && \ sed -i "s/git+git/git+https/g" requirements.txt && \ - pip3 install --break-system-packages --no-cache-dir -r requirements.txt && \ - cp /root/dist/asa_server.py /opt/ciscoasa_honeypot && \ - chown -R ciscoasa:ciscoasa /opt/ciscoasa_honeypot && \ + pip3 install --break-system-packages pyinstaller && \ + pip3 install --break-system-packages --no-cache-dir -r requirements.txt +WORKDIR /opt/ciscoasa_honeypot +RUN pyinstaller asa_server.py --add-data "./asa:./asa" # -# Clean up - apk del --purge build-base \ - git \ - libffi-dev \ - openssl-dev \ - python3-dev && \ - rm -rf /root/* \ - /opt/ciscoasa_honeypot/.git \ - /var/cache/apk/* +FROM alpine:3.20 +COPY --from=builder /opt/ciscoasa_honeypot/dist/ /opt/ # # Start ciscoasa STOPSIGNAL SIGINT -WORKDIR /tmp/ciscoasa/ -USER ciscoasa:ciscoasa -CMD cp -R /opt/ciscoasa_honeypot/* /tmp/ciscoasa && exec python3 asa_server.py --ike-port 5000 --enable_ssl --port 8443 --verbose >> /var/log/ciscoasa/ciscoasa.log 2>&1 +WORKDIR /opt/asa_server/ +USER 2000:2000 +CMD ./asa_server --ike-port 5000 --enable_ssl --port 8443 --verbose >> /var/log/ciscoasa/ciscoasa.log 2>&1 diff --git a/docker/ciscoasa/dist/asa_server.py b/docker/ciscoasa/dist/asa_server.py deleted file mode 100644 index e1883870..00000000 --- a/docker/ciscoasa/dist/asa_server.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import os -import time -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 -import datetime - - -class NonBlockingHTTPServer(ThreadingMixIn, HTTPServer): - pass - -class hpflogger: - def __init__(self, hpfserver, hpfport, hpfident, hpfsecret, hpfchannel, serverid, verbose): - self.hpfserver=hpfserver - self.hpfport=hpfport - self.hpfident=hpfident - self.hpfsecret=hpfsecret - self.hpfchannel=hpfchannel - self.serverid=serverid - self.hpc=None - self.verbose=verbose - if (self.hpfserver and self.hpfport and self.hpfident and self.hpfport and self.hpfchannel and self.serverid): - import hpfeeds - try: - self.hpc = hpfeeds.new(self.hpfserver, self.hpfport, self.hpfident, self.hpfsecret) - logger.debug("Logging to hpfeeds using server: {0}, channel {1}.".format(self.hpfserver, self.hpfchannel)) - except (hpfeeds.FeedException, socket.error, hpfeeds.Disconnect): - logger.critical("hpfeeds connection not successful") - - def log(self, level, message): - if self.hpc: - if level in ['debug', 'info'] and not self.verbose: - return - self.hpc.publish(self.hpfchannel, "["+self.serverid+"] ["+level+"] ["+datetime.datetime.now().isoformat() +"] " + str(message)) - - -def header_split(h): - return [list(map(str.strip, l.split(': ', 1))) for l in h.strip().splitlines()] - - -class WebLogicHandler(SimpleHTTPRequestHandler): - logger = None - hpfl = 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.now().isoformat(), - self.client_address[0], - format % args)) - self.hpfl.log('debug', "%s - - [%s] %s" % - (self.client_address[0], - self.log_date_time_string(), - 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) - - # hpfeeds options - @click.option('--hpfserver', default=os.environ.get('HPFEEDS_SERVER'), help='HPFeeds Server') - @click.option('--hpfport', default=os.environ.get('HPFEEDS_PORT'), help='HPFeeds Port', type=click.INT) - @click.option('--hpfident', default=os.environ.get('HPFEEDS_IDENT'), help='HPFeeds Ident') - @click.option('--hpfsecret', default=os.environ.get('HPFEEDS_SECRET'), help='HPFeeds Secret') - @click.option('--hpfchannel', default=os.environ.get('HPFEEDS_CHANNEL'), help='HPFeeds Channel') - @click.option('--serverid', default=os.environ.get('SERVERID'), help='Verbose logging') - - - def start(host, port, ike_port, enable_ssl, cert, verbose, hpfserver, hpfport, hpfident, hpfsecret, hpfchannel, serverid): - """ - A low interaction honeypot for the Cisco ASA component capable of detecting CVE-2018-0101, - a DoS and remote code execution vulnerability - """ - - hpfl=hpflogger(hpfserver, hpfport, hpfident, hpfsecret, hpfchannel, serverid, verbose) - - def alert(cls, host, port, payloads): - logger.critical({ - 'timestamp': datetime.datetime.utcnow().isoformat(), - 'src_ip': host, - 'src_port': port, - 'payload_printable': payloads, - }) - #log to hpfeeds - hpfl.log("critical", { - 'src': host, - 'spt': port, - 'data': payloads, - }) - - if verbose: - logger.setLevel(logging.DEBUG) - - requestHandler = WebLogicHandler - requestHandler.alert_function = alert - requestHandler.logger = logger - requestHandler.hpfl = hpfl - - def log_date_time_string(): - """Return the current time formatted for logging.""" - now = datetime.datetime.now().isoformat() - return now - - def ike(): - ike_server.start(host, ike_port, alert, logger, hpfl) - 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)) - hpfl.log('info', 'Starting server on port {:d}/tcp, use to stop'.format(port)) - - try: - httpd.serve_forever() - except KeyboardInterrupt: - pass - logger.info('Stopping server.') - hpfl.log('info', 'Stopping server.') - - httpd.server_close() - - start()