add file header bump version add ssh tarpit, add fail2ban ssh config, harden ssh fix bug, supervisor conf is a file not a template initial http tarpit Co-authored-by: Jonathan Cobb <jonathan@kyuss.org> Reviewed-on: #56tags/v1.2.0
@@ -1 +1 @@ | |||
bubble.version=Adventure 1.1.4 | |||
bubble.version=Adventure 1.2.0 |
@@ -18,7 +18,7 @@ | |||
- name: Ensure mitmproxy user owns all mitmproxy files | |||
shell: chown -R mitmproxy /home/mitmproxy/mitmproxy | |||
- name: Install mitmproxy1 supervisor conf file | |||
- name: Install mitm8888 supervisor conf file | |||
template: | |||
src: supervisor_mitmproxy.conf.j2 | |||
dest: /etc/supervisor/conf.d/mitm8888.conf | |||
@@ -28,7 +28,7 @@ | |||
vars: | |||
port: 8888 | |||
- name: Install mitmproxy2 supervisor conf file | |||
- name: Install mitm9999 supervisor conf file | |||
template: | |||
src: supervisor_mitmproxy.conf.j2 | |||
dest: /etc/supervisor/conf.d/mitm9999.conf | |||
@@ -1,5 +1,6 @@ | |||
common | |||
firewall | |||
tarpit | |||
nginx | |||
algo | |||
mitmproxy | |||
@@ -0,0 +1,9 @@ | |||
Port 1202 | |||
LoginGraceTime 10 | |||
PasswordAuthentication no | |||
PermitEmptyPasswords no | |||
ChallengeResponseAuthentication no | |||
KerberosAuthentication no | |||
GSSAPIAuthentication no | |||
X11Forwarding no | |||
PermitUserEnvironment no |
@@ -0,0 +1,5 @@ | |||
[sshd] | |||
mode = aggressive | |||
port = 1202 | |||
logpath = %(sshd_log)s | |||
backend = %(sshd_backend)s |
@@ -90,15 +90,34 @@ | |||
dest: /usr/local/bin/bubble_peer_manager.py | |||
owner: root | |||
group: root | |||
mode: 0555 | |||
mode: 0550 | |||
when: fw_enable_admin | |||
- name: Install supervisor conf file for port manager | |||
- name: Install supervisor conf file for peer manager | |||
copy: | |||
src: supervisor_bubble_peer_manager.conf | |||
dest: /etc/supervisor/conf.d/bubble_peer_manager.conf | |||
owner: root | |||
group: root | |||
mode: 0550 | |||
when: fw_enable_admin | |||
- name: Install SSH hardening settings | |||
copy: | |||
src: bubble_sshd.conf | |||
dest: /etc/ssh/ssh_config.d/bubble_sshd.conf | |||
owner: root | |||
group: root | |||
mode: 0400 | |||
- name: Install SSH fail2ban settings | |||
copy: | |||
src: jail.local | |||
dest: /etc/fail2ban/jail.local | |||
owner: root | |||
group: root | |||
mode: 0400 | |||
- include: rules.yml | |||
- supervisorctl: | |||
@@ -17,15 +17,18 @@ | |||
comment: Allow related and established connections | |||
become: yes | |||
- name: Allow SSH | |||
- name: Allow SSH on ports 22 and 1202 | |||
iptables: | |||
chain: INPUT | |||
protocol: tcp | |||
destination_port: 22 | |||
destination_port: "{{ item }}" | |||
ctstate: NEW | |||
syn: match | |||
jump: ACCEPT | |||
comment: Accept new SSH connections | |||
with_items: | |||
- 22 | |||
- 1202 | |||
become: yes | |||
when: fw_enable_ssh | |||
@@ -64,6 +64,7 @@ LOCAL_IPS = [] | |||
for local_ip in subprocess.check_output(['hostname', '-I']).split(): | |||
LOCAL_IPS.append(local_ip.decode()) | |||
TARPIT_PORT = 8080 | |||
VPN_IP4_CIDR = IPNetwork(wireguard_network_ipv4) | |||
VPN_IP6_CIDR = IPNetwork(wireguard_network_ipv6) | |||
@@ -469,6 +470,24 @@ def health_check_response(flow): | |||
flow.response.stream = lambda chunks: [b'OK\n'] | |||
def tarpit_response(flow, host): | |||
# if bubble_log.isEnabledFor(DEBUG): | |||
# bubble_log.debug('health_check_response: special bubble health check request, responding with OK') | |||
response_headers = nheaders.Headers() | |||
response_headers[HEADER_LOCATION] = 'http://'+host+':'+str(TARPIT_PORT)+'/admin/index.php' | |||
if flow.response is None: | |||
flow.response = http.HTTPResponse(http_version='HTTP/1.1', | |||
status_code=301, | |||
reason='Moved Permanently', | |||
headers=response_headers, | |||
content=b'') | |||
else: | |||
flow.response.headers = nheaders.Headers() | |||
flow.response.headers = response_headers | |||
flow.response.status_code = 301 | |||
flow.response.reason = 'Moved Permanently' | |||
def include_request_headers(path): | |||
return '/followAndApplyRegex' in path | |||
@@ -32,7 +32,7 @@ from mitmproxy.net.http import headers as nheaders | |||
from bubble_api import bubble_matchers, bubble_activity_log, \ | |||
CTX_BUBBLE_MATCHERS, CTX_BUBBLE_SPECIAL, CTX_BUBBLE_ABORT, CTX_BUBBLE_LOCATION, \ | |||
CTX_BUBBLE_PASSTHRU, CTX_BUBBLE_FLEX, CTX_BUBBLE_REQUEST_ID, add_flow_ctx, parse_host_header, \ | |||
is_bubble_special_path, is_bubble_health_check, health_check_response, \ | |||
is_bubble_special_path, is_bubble_health_check, health_check_response, tarpit_response,\ | |||
is_bubble_request, is_sage_request, is_not_from_vpn, is_flex_domain | |||
from bubble_config import bubble_host, bubble_host_alias | |||
from bubble_flex import new_flex_flow | |||
@@ -170,9 +170,9 @@ class Rerouter: | |||
elif is_not_from_vpn(client_addr): | |||
# todo: add to fail2ban | |||
if bubble_log.isEnabledFor(WARNING): | |||
bubble_log.warning('bubble_handle_request: returning 404 for non-VPN client='+client_addr+', url='+log_url+' host='+host) | |||
bubble_activity_log(client_addr, server_addr, 'http_abort_non_vpn', fqdns) | |||
add_flow_ctx(flow, CTX_BUBBLE_ABORT, 404) | |||
bubble_log.warning('bubble_handle_request: sending to tarpit: non-VPN client='+client_addr+', url='+log_url+' host='+host) | |||
bubble_activity_log(client_addr, server_addr, 'http_tarpit_non_vpn', fqdns) | |||
tarpit_response(flow, host) | |||
return None | |||
if is_bubble_special_path(path): | |||
@@ -0,0 +1,89 @@ | |||
#!/usr/bin/python3 | |||
# | |||
# Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ | |||
# | |||
# Adapted from: https://nullprogram.com/blog/2019/03/22/ | |||
# | |||
import asyncio | |||
import random | |||
import os | |||
import sys | |||
import logging | |||
from logging import INFO, DEBUG, WARNING, ERROR, CRITICAL | |||
from pathlib import Path | |||
TARPIT_LOG = '/var/log/bubble/http_tarpit.log' | |||
TARPIT_LOG_LEVEL_FILE = '/home/tarpit/http_tarpit_log_level.txt' | |||
TARPIT_LOG_LEVEL_ENV_VAR = 'HTTP_TARPIT_LOG_LEVEL' | |||
DEFAULT_TARPIT_LOG_LEVEL = 'INFO' | |||
TARPIT_LOG_LEVEL = None | |||
TARPIT_PORT_FILE = '/home/tarpit/http_tarpit_port.txt' | |||
TARPIT_PORT_ENV_VAR = 'HTTP_TARPIT_PORT' | |||
DEFAULT_TARPIT_PORT = '8080' | |||
tarpit_log = logging.getLogger(__name__) | |||
try: | |||
TARPIT_LOG_LEVEL = Path(TARPIT_LOG_LEVEL_FILE).read_text().strip() | |||
except IOError: | |||
print('error reading log level from '+TARPIT_LOG_LEVEL_FILE+', checking env var '+TARPIT_LOG_LEVEL_ENV_VAR, file=sys.stderr, flush=True) | |||
TARPIT_LOG_LEVEL = os.getenv(TARPIT_LOG_LEVEL_ENV_VAR, DEFAULT_TARPIT_LOG_LEVEL) | |||
TARPIT_NUMERIC_LOG_LEVEL = getattr(logging, TARPIT_LOG_LEVEL.upper(), None) | |||
if not isinstance(TARPIT_NUMERIC_LOG_LEVEL, int): | |||
print('Invalid log level: ' + TARPIT_LOG_LEVEL + ' - using default '+DEFAULT_TARPIT_LOG_LEVEL, file=sys.stderr, flush=True) | |||
TARPIT_NUMERIC_LOG_LEVEL = getattr(logging, DEFAULT_TARPIT_LOG_LEVEL.upper(), None) | |||
try: | |||
with open(TARPIT_LOG, 'w+') as f: | |||
logging.basicConfig(format='%(asctime)s - [%(module)s:%(lineno)d] - %(levelname)s: %(message)s', filename=TARPIT_LOG, level=TARPIT_NUMERIC_LOG_LEVEL) | |||
except IOError: | |||
logging.basicConfig(format='%(asctime)s - [%(module)s:%(lineno)d] - %(levelname)s: %(message)s', stream=sys.stdout, level=TARPIT_NUMERIC_LOG_LEVEL) | |||
tarpit_log = logging.getLogger(__name__) | |||
if tarpit_log.isEnabledFor(INFO): | |||
tarpit_log.info('tarpit initialized, default log level = '+logging.getLevelName(TARPIT_NUMERIC_LOG_LEVEL)) | |||
TARPIT_PORT = 8080 | |||
try: | |||
TARPIT_PORT = int(Path(TARPIT_PORT_FILE).read_text().strip()) | |||
except IOError: | |||
print('error reading port from '+TARPIT_PORT_FILE+', checking env var '+TARPIT_PORT_ENV_VAR, file=sys.stderr, flush=True) | |||
TARPIT_PORT = int(os.getenv(TARPIT_PORT_ENV_VAR, DEFAULT_TARPIT_PORT)) | |||
TRAP_COUNT = 0 | |||
async def handler(_reader, writer): | |||
global TRAP_COUNT | |||
TRAP_COUNT = TRAP_COUNT + 1 | |||
peer_addr = writer.get_extra_info('socket').getpeername()[0] | |||
if tarpit_log.isEnabledFor(INFO): | |||
tarpit_log.info('trapped '+peer_addr+' - trap count: ' + str(TRAP_COUNT)) | |||
writer.write(b'HTTP/1.1 200 OK\r\n') | |||
try: | |||
while True: | |||
header = random.randint(0, 2**32) | |||
value = random.randint(0, 2**32) | |||
await asyncio.sleep(3 + (header % 4)) | |||
writer.write(b'X-WOPR-%x: %x\r\n' % (header, value)) | |||
await writer.drain() | |||
except ConnectionResetError: | |||
TRAP_COUNT = TRAP_COUNT - 1 | |||
if tarpit_log.isEnabledFor(INFO): | |||
tarpit_log.info('dropped '+peer_addr+' - trap count: ' + str(TRAP_COUNT)) | |||
pass | |||
async def main(): | |||
if tarpit_log.isEnabledFor(INFO): | |||
tarpit_log.info('starting HTTP tarpit on port '+str(TARPIT_PORT)) | |||
server = await asyncio.start_server(handler, '0.0.0.0', TARPIT_PORT) | |||
async with server: | |||
await server.serve_forever() | |||
asyncio.run(main()) |
@@ -0,0 +1,87 @@ | |||
#!/usr/bin/python3 | |||
# | |||
# Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ | |||
# | |||
# Adapted from: https://nullprogram.com/blog/2019/03/22/ | |||
# | |||
import asyncio | |||
import random | |||
import os | |||
import sys | |||
import logging | |||
from logging import INFO, DEBUG, WARNING, ERROR, CRITICAL | |||
from pathlib import Path | |||
TARPIT_LOG = '/var/log/bubble/ssh_tarpit.log' | |||
TARPIT_LOG_LEVEL_FILE = '/home/tarpit/ssh_tarpit_log_level.txt' | |||
TARPIT_LOG_LEVEL_ENV_VAR = 'SSH_TARPIT_LOG_LEVEL' | |||
DEFAULT_TARPIT_LOG_LEVEL = 'INFO' | |||
TARPIT_LOG_LEVEL = None | |||
TARPIT_PORT_FILE = '/home/tarpit/ssh_tarpit_port.txt' | |||
TARPIT_PORT_ENV_VAR = 'SSH_TARPIT_PORT' | |||
DEFAULT_TARPIT_PORT = '22' | |||
tarpit_log = logging.getLogger(__name__) | |||
try: | |||
TARPIT_LOG_LEVEL = Path(TARPIT_LOG_LEVEL_FILE).read_text().strip() | |||
except IOError: | |||
print('error reading log level from '+TARPIT_LOG_LEVEL_FILE+', checking env var '+TARPIT_LOG_LEVEL_ENV_VAR, file=sys.stderr, flush=True) | |||
TARPIT_LOG_LEVEL = os.getenv(TARPIT_LOG_LEVEL_ENV_VAR, DEFAULT_TARPIT_LOG_LEVEL) | |||
TARPIT_NUMERIC_LOG_LEVEL = getattr(logging, TARPIT_LOG_LEVEL.upper(), None) | |||
if not isinstance(TARPIT_NUMERIC_LOG_LEVEL, int): | |||
print('Invalid log level: ' + TARPIT_LOG_LEVEL + ' - using default '+DEFAULT_TARPIT_LOG_LEVEL, file=sys.stderr, flush=True) | |||
TARPIT_NUMERIC_LOG_LEVEL = getattr(logging, DEFAULT_TARPIT_LOG_LEVEL.upper(), None) | |||
try: | |||
with open(TARPIT_LOG, 'w+') as f: | |||
logging.basicConfig(format='%(asctime)s - [%(module)s:%(lineno)d] - %(levelname)s: %(message)s', filename=TARPIT_LOG, level=TARPIT_NUMERIC_LOG_LEVEL) | |||
except IOError: | |||
logging.basicConfig(format='%(asctime)s - [%(module)s:%(lineno)d] - %(levelname)s: %(message)s', stream=sys.stdout, level=TARPIT_NUMERIC_LOG_LEVEL) | |||
tarpit_log = logging.getLogger(__name__) | |||
if tarpit_log.isEnabledFor(INFO): | |||
tarpit_log.info('tarpit initialized, default log level = '+logging.getLevelName(TARPIT_NUMERIC_LOG_LEVEL)) | |||
TARPIT_PORT = 8080 | |||
try: | |||
TARPIT_PORT = int(Path(TARPIT_PORT_FILE).read_text().strip()) | |||
except IOError: | |||
print('error reading port from '+TARPIT_PORT_FILE+', checking env var '+TARPIT_PORT_ENV_VAR, file=sys.stderr, flush=True) | |||
TARPIT_PORT = int(os.getenv(TARPIT_PORT_ENV_VAR, DEFAULT_TARPIT_PORT)) | |||
TRAP_COUNT = 0 | |||
async def handler(_reader, writer): | |||
global TRAP_COUNT | |||
TRAP_COUNT = TRAP_COUNT + 1 | |||
peer_addr = writer.get_extra_info('socket').getpeername()[0] | |||
if tarpit_log.isEnabledFor(INFO): | |||
tarpit_log.info('trapped '+peer_addr+' - trap count: ' + str(TRAP_COUNT)) | |||
try: | |||
while True: | |||
val = random.randint(0, 2 ** 32) | |||
await asyncio.sleep(6 + (val % 4)) | |||
writer.write(b'%x\r\n' % val) | |||
await writer.drain() | |||
except ConnectionResetError: | |||
TRAP_COUNT = TRAP_COUNT - 1 | |||
if tarpit_log.isEnabledFor(INFO): | |||
tarpit_log.info('dropped '+peer_addr+' - trap count: ' + str(TRAP_COUNT)) | |||
pass | |||
async def main(): | |||
if tarpit_log.isEnabledFor(INFO): | |||
tarpit_log.info('starting SSH tarpit on port '+str(TARPIT_PORT)) | |||
server = await asyncio.start_server(handler, '0.0.0.0', TARPIT_PORT) | |||
async with server: | |||
await server.serve_forever() | |||
asyncio.run(main()) |
@@ -0,0 +1,6 @@ | |||
[program:http_tarpit] | |||
stdout_logfile = /dev/null | |||
stderr_logfile = /dev/null | |||
command=sudo -u tarpit /home/tarpit/bubble_http_tarpit.py | |||
stopsignal=QUIT |
@@ -0,0 +1,6 @@ | |||
[program:ssh_tarpit] | |||
stdout_logfile = /dev/null | |||
stderr_logfile = /dev/null | |||
command=/home/tarpit/bubble_ssh_tarpit.py | |||
stopsignal=QUIT |
@@ -0,0 +1,54 @@ | |||
# | |||
# Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ | |||
# | |||
- name: Create tarpit user | |||
user: | |||
name: tarpit | |||
comment: tarpit user | |||
shell: /bin/false | |||
system: yes | |||
home: /home/tarpit | |||
groups: bubble-log | |||
- name: Copy bubble_ssh_tarpit script | |||
copy: | |||
src: bubble_ssh_tarpit.py | |||
dest: /home/tarpit/bubble_ssh_tarpit.py | |||
owner: tarpit | |||
group: tarpit | |||
mode: 0500 | |||
- name: Copy bubble_http_tarpit script | |||
copy: | |||
src: bubble_http_tarpit.py | |||
dest: /home/tarpit/bubble_http_tarpit.py | |||
owner: tarpit | |||
group: tarpit | |||
mode: 0500 | |||
- name: Install ssh tarpit supervisor conf file | |||
copy: | |||
src: supervisor_ssh_tarpit.conf | |||
dest: /etc/supervisor/conf.d/ssh_tarpit.conf | |||
owner: root | |||
group: root | |||
mode: 0400 | |||
- name: Install http tarpit supervisor conf file | |||
copy: | |||
src: supervisor_http_tarpit.conf | |||
dest: /etc/supervisor/conf.d/http_tarpit.conf | |||
owner: root | |||
group: root | |||
mode: 0400 | |||
- name: Allow HTTP tarpit port | |||
iptables: | |||
chain: INPUT | |||
protocol: tcp | |||
destination_port: 8080 | |||
ctstate: NEW | |||
syn: match | |||
jump: ACCEPT | |||
comment: Accept new connections on HTTP tarpit port | |||
become: yes |