From ec39aad0a01d852f1f74d6e90a99b2405a0738be Mon Sep 17 00:00:00 2001 From: Michel Oosterhof Date: Fri, 14 May 2021 15:43:28 +0800 Subject: [PATCH] 9may (#1560) * fix MySQL error handling * fix tar command * type hinting for proxy --- INSTALL.rst | 4 +- Makefile | 2 +- README.rst | 2 +- docs/BACKEND_POOL.rst | 4 +- honeyfs/etc/shadow | Bin 750 -> 7168 bytes src/backend_pool/README.md | 30 ++++++------- src/backend_pool/libvirt/backend_service.py | 20 +++++---- src/backend_pool/libvirt/guest_handler.py | 22 ++++++---- src/backend_pool/libvirt/network_handler.py | 8 ++-- src/backend_pool/nat.py | 6 +-- src/backend_pool/pool_server.py | 10 +++-- src/backend_pool/pool_service.py | 34 ++++++++------- src/backend_pool/ssh_exec.py | 2 +- src/backend_pool/telnet_exec.py | 22 +++++----- src/cowrie/commands/tar.py | 5 ++- src/cowrie/core/output.py | 10 ++++- src/cowrie/output/mysql.py | 14 +++--- src/cowrie/pool_interface/client.py | 2 +- src/cowrie/ssh/forwarding.py | 8 +++- src/cowrie/ssh_proxy/client_transport.py | 5 +++ .../ssh_proxy/protocols/base_protocol.py | 20 ++++----- src/cowrie/ssh_proxy/protocols/exec_term.py | 8 ++-- src/cowrie/ssh_proxy/protocols/sftp.py | 41 ++++++++++-------- src/cowrie/ssh_proxy/protocols/term.py | 28 ++++++------ 24 files changed, 172 insertions(+), 135 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index ccbba022..ae3a75e0 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -176,7 +176,7 @@ Installing Backend Pool dependencies (OPTIONAL) *********************************************** If you want to use the proxy functionality combined with the automatic -backend pool, you need to install some dependencies, namely qemu, libvirt, +backend pool, you need to install some dependencies, namely QEMU, libvirt, and their Python interface. In Debian/Ubuntu:: $ sudo apt-get install qemu qemu-system-arm qemu-system-x86 libvirt-dev libvirt-daemon libvirt-daemon-system libvirt-clients nmap @@ -185,7 +185,7 @@ Then install the Python API to run the backend pool:: (cowrie-env) $ pip install libvirt-python==6.4.0 -To allow Qemu to use disk images and snapshots, set it to run with the user and group of the user running the pool +To allow QEMU to use disk images and snapshots, set it to run with the user and group of the user running the pool (usually called 'cowrie' too:: $ sudo vim /etc/libvirt/qemu.conf diff --git a/Makefile b/Makefile index 605f88d4..7e1de01f 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ lint: .PHONY: clean clean: - rm -rf _trial_temp build dist + rm -rf _trial_temp build dist src/_trial_temp src/Cowrie.egg-info make -C docs clean .PHONY: pre-commit diff --git a/README.rst b/README.rst index b51e161e..35cb148e 100644 --- a/README.rst +++ b/README.rst @@ -43,7 +43,7 @@ Features * Or proxy SSH and telnet to another system * Run as a pure telnet and ssh proxy with monitoring - * Or let Cowrie manage a pool of Qemu emualted servers to provide the systems to login to + * Or let Cowrie manage a pool of QEMU emulated servers to provide the systems to login to For both settings: diff --git a/docs/BACKEND_POOL.rst b/docs/BACKEND_POOL.rst index 6765529c..b34a062d 100644 --- a/docs/BACKEND_POOL.rst +++ b/docs/BACKEND_POOL.rst @@ -135,8 +135,8 @@ A set of guest (VM) parameters can be defined as we explain below: * **guest_image_path**: the base image upon which all VMs are created from * **guest_hypervisor**: the hypervisor used; if you have an older machine or the emulated - architecture is different from the host one, then use software-based "qemu"; however, - if you are able to, use "kvm", it's **much** faster. + architecture is different from the host one, then use software-based "QEMU"; however, + if you are able to, use "KVM", it's **much** faster. * **guest_memory**: memory assigned to the guest; choose a value considering the number of guests you'll have running in total (``pool_max_vms``) diff --git a/honeyfs/etc/shadow b/honeyfs/etc/shadow index 5894a98fe5453d11d9600bcd3fa558afa6a776e3..72056f9c7a558371fadf48ba8912f884b720e2a1 100644 GIT binary patch literal 7168 zcmeHLe{bBz5%u5v6a%zCWyRv16iGpax~(Hywv#AQd@|aiD11wDNp4JXncd}`4)BNX zo89G!k{m!P5FjXguys6gclOQ9n_qnQ>cz|RSE;e?$#3?V&*#VUIX%JmH^)c*dwy^j zzUTA(gM))3+CQ8h9vvU=9~{nUzW??9;n5SC|7M6Lf(Pq_0kl@-wQMF2?p$J7S>Cxf z{R!pp8I_{xqu=xOsa_i;lRx-Rl6-PTtd#xqA^DUmlQ%L~{#q9@*DG8m|By|tTS-on zt0jf~^jf($Lqut+ zZ*(U&y>=u^EOaR#*21y8sM|w<>?TRn273jyvQR_Ide*9{Np(&dkvpY3x{ZM9h#)DX zRaGYo-qh%dGBY^I@N-GRIgx*aU?68jE!fsfr6#>JSc+pQ+xR76i!>>{?M66L7%jX) zJ6_OJQZAA0&ZZ|a)m60ghtOJ^kG2<}))wu0y@o|Opi^zCnPo7u0PIW(HyCLjgKd{i zYpt!1bu)C450Pa>WbjTQOEEO==}AIFyL2W#!j`I$k$r18I^&Sn6XZPu{Xf?mo@!Bt1;96UxkZr}rsMzBS7c{3EU2oNr{wJM&bOFoSS zY-Llf5zC#kWJ3-c{;|grK;V(~`Ay6y#yG8vN4+whQ}kOe5V$&{lM!LiAIw zHGr4&)2s9MbVCl#4F)+~v&fY~rebynQuy0QkW6`_~$IB*9*eCK@ z=HN&Rn-z#a`axIFy<)2s%D|C(4(L8w!d7cx3R;|l7&kk- zMg6^PIoy$HmC_Q4wBh=6lO-mpSFCI0khXE0nT;!9B0lN6#l??oe2;W%DmXJ|#{fMX zfycyJ*g?!17U5kkRzNS{U!>Muy3&M50S|<6Ai^($ z%0D6!nYxuiE{UNg$eGPELdH4)k&u+=j|e$0oU_-6|57`gYY!rn?5H;6`ds}P@Y zQ6YPYlGo)*+GmJY#7dOq!KCimeU@mq>^#dFoq=v#%uxq!bm8r>TyX*6`0)LXwC?0F zdhXJa6LvT>%}WIFUS!9gEJsbLs=#w-Z4IA{geh&>Gp{9M-qx&;H{l+Sgp*v6v9)+nj;shKplSA5K;#^to@d*}6#M*j zX=e{`k26w^?upwtZ351%TE{to*?b2iuagpm5mGq)=eh^GK{*u5*c1333%j95$hD=- zU9)8MMwo0aW$oazHfz}%q;y|0GJ!ma)N*@qt6lK`-KNyLcUI66BaFLu)Jm8cjvu`g zsr9qf9fQNv)Y&*3=)v^^=8rC9P*@4IhS7dr1cb5#mR-nfKvD4fw;NP8p6T3X#!Ky_ zFe1(Q+Qm8Y0re|jxDh#aBGwCL_kuXAODPT4+R|uDK=ARlMS?^5w>KRRU6aDhUS-&~ z=nRhLZSiQ@eNM)&anP!^hxVR^lOpSjIAQ}0Q)wT`46gs<)^IGS>c_fp_j9~w)hB(! z|8QdLIO2h!>);5A?a9>dD6ql_S|D?v6h9(2p8A3Zs-WA(m>HY**mY9DLeX>OMqsSu z@vn%pBYK!z_P)3?B+;wMq>)k>;k!Wg*p!bZgL}{chW_t{H;|0R9b+Qq2#_eY!md_m zZh91W6d26CZcN37cI7QcQ zR5qZCyjL`nZN8TVT=teyeW!@A1v9wy;~UJsl8x@UoBCd%n`GOgCBxJ)`U%rVCcr(g>m|c_ zElsO_8PG`V6!=|@KZEcq?oVT1`iyvfM1OA`N_8jK+=%mfI)Z#CAy*aObl%k_u;Px)gayAHPpA<*>j&CP+!-LE&a^Ln9A>0rM9XYO!cfP)^j|Knd+SP&&mX?zU^ebC#U+_ZV&_1Ip{?7#Eo zBDJgPZyEH->*t}jVM!mtjt5ByM{nP|-8+1I;?J(?u`a|~#K&8=Y216H;0dR zJ}V&}53BR5zWb+#2mJoy?%A`T_SiF2M)dV869s0jEtar*0_E4mCBNqvreCmqvA`D# Le6hg)i3R=(WFQuk literal 750 zcmZ{i%W{G+6o&VGi_Wm>OqGkGX6L=u8(3CXa<VavH%n7Yfq?*Nt3-xoSfc zHj46;MGPrA7B~_DBk(S}U|nZOUZc!jJAA#|npZozV7<1%QD_?Gd_%vE&+d+bU^-)idPwo?Q-0!Q4<9UT^Jhod$H?PA0wr=3^6@vB+@7LbRvkh6P EKkfs@z5oCK diff --git a/src/backend_pool/README.md b/src/backend_pool/README.md index 3fffa417..d34ab3c2 100644 --- a/src/backend_pool/README.md +++ b/src/backend_pool/README.md @@ -1,43 +1,43 @@ -qemu/libvirt Python examples to handle a guest +QEMU/libvirt Python examples to handle a guest # Developer Guide We'll start by looking at the classes that compose the Backend Pool, from "outermost" to the inner, specific classes. ## pool_server.py -The main interface of the backend pool is exposed as a TCP server in _pool\_server.py_. The protocol is a very simple +The main interface of the backend pool is exposed as a TCP server in _pool\_server.py_. The protocol is a very simple wire protocol, always composed of an op-code, a status code (for responses), and any needed data thereafter. ## pool_service.py The server interfaces exposes a producer-consumer infinite loop that runs on _pool\_service.py_. -The **producer** is an infinite loop started by the server, and runs every 5 seconds. It creates VMs up to the -configured limit, checks which VMs become available (by testing if they accept SSH and/or Telnet connections), and +The **producer** is an infinite loop started by the server, and runs every 5 seconds. It creates VMs up to the +configured limit, checks which VMs become available (by testing if they accept SSH and/or Telnet connections), and destroys VMs that are no longer needed. -**Consumer** methods are called by server request, and basically involve requesting and freeing VMs. All operations on -shared data in the producer-consumer are guarded by a lock, since there may be concurrent requests. The lock protects -the _guests_ list, which contains references for each VM backend (in our case libvirt/qemu instances). +**Consumer** methods are called by server request, and basically involve requesting and freeing VMs. All operations on +shared data in the producer-consumer are guarded by a lock, since there may be concurrent requests. The lock protects +the _guests_ list, which contains references for each VM backend (in our case libvirt/QEMU instances). -Since we won't be dealing with a very large number of VMs (never more than 100, we find that a single simple lock is +Since we won't be dealing with a very large number of VMs (never more than 100, we find that a single simple lock is enough. The Pool Service expects to find a "backend service" with a given interface: -* A method to initialise the backend interface and environment (start_backend), stop it and destroy the current +* A method to initialise the backend interface and environment (start_backend), stop it and destroy the current environment (stop_backend), and shutdown it permanently for the current execution (shutdown_backend). * A method to create a new guest (create_guest) * A method to destroy a guest (destroy_guest) -Currently the service supports a libvirt/qemu backend. However, by splitting the logic from generic guest handling / -interaction with main Cowrie, from the logic to create guests in a low-level perspective, we hope to ease development +Currently the service supports a libvirt/QEMU backend. However, by splitting the logic from generic guest handling / +interaction with main Cowrie, from the logic to create guests in a low-level perspective, we hope to ease development of different kinds of backends in the future. ## libvirt classes -The main class for libvirt is _backend\_service.py_, and implements the interface discussed above. Guest, network and +The main class for libvirt is _backend\_service.py_, and implements the interface discussed above. Guest, network and snapshot handlers deal with those specific components of libvirt's handling. -Initialising libvirt involves connecting to the running system daemon, creating a network filter to restrict guest's +Initialising libvirt involves connecting to the running system daemon, creating a network filter to restrict guest's Internet access, and creating a "cowrie" network in libvirt. -Guest creation is started by creating a snapshot from the base qcow2 image defined in the configs, and instantiating -a guest from the XML provided. The Guest Handler replaces templates ("{guest_name}") with user configs for the wanted +Guest creation is started by creating a snapshot from the base qcow2 image defined in the configs, and instantiating +a guest from the XML provided. The Guest Handler replaces templates ("{guest_name}") with user configs for the wanted guest. If the XML provided does not contain templates, then no replacement takes place, naturally. diff --git a/src/backend_pool/libvirt/backend_service.py b/src/backend_pool/libvirt/backend_service.py index 14201a4d..f371a119 100644 --- a/src/backend_pool/libvirt/backend_service.py +++ b/src/backend_pool/libvirt/backend_service.py @@ -1,5 +1,6 @@ # Copyright (c) 2019 Guilherme Borges # See the COPYRIGHT file for more information + import os import random import sys @@ -12,6 +13,8 @@ import backend_pool.libvirt.network_handler import backend_pool.util from cowrie.core.config import CowrieConfig +LIBVIRT_URI = "qemu:///system" + class LibvirtError(Exception): pass @@ -23,11 +26,12 @@ class LibvirtBackendService: import libvirt # open connection to libvirt - self.conn = libvirt.open("qemu:///system") + self.conn = libvirt.open(LIBVIRT_URI) if self.conn is None: log.msg( eventid="cowrie.backend_pool.qemu", - format="Failed to open connection to qemu:///system", + format="Failed to open connection to %(uri)s", + uri=LIBVIRT_URI, ) raise LibvirtError() @@ -35,19 +39,19 @@ class LibvirtBackendService: self.network = None # signals backend is ready to be operated - self.ready = False + self.ready: bool = False # table to associate IPs and MACs - seed = random.randint(0, sys.maxsize) + seed: int = random.randint(0, sys.maxsize) self.network_table = backend_pool.util.generate_network_table(seed) log.msg( - eventid="cowrie.backend_pool.qemu", format="Connection to Qemu established" + eventid="cowrie.backend_pool.qemu", format="Connection to QEMU established" ) def start_backend(self): """ - Initialises Qemu/libvirt environment needed to run guests. Namely starts networks and network filters. + Initialises QEMU/libvirt environment needed to run guests. Namely starts networks and network filters. """ # create a network filter self.filter = backend_pool.libvirt.network_handler.create_filter(self.conn) @@ -62,7 +66,7 @@ class LibvirtBackendService: def stop_backend(self): log.msg( - eventid="cowrie.backend_pool.qemu", format="Doing Qemu clean shutdown..." + eventid="cowrie.backend_pool.qemu", format="Doing QEMU clean shutdown..." ) self.ready = False @@ -74,7 +78,7 @@ class LibvirtBackendService: log.msg( eventid="cowrie.backend_pool.qemu", - format="Connection to Qemu closed successfully", + format="Connection to QEMU closed successfully", ) def get_mac_ip(self, ip_tester): diff --git a/src/backend_pool/libvirt/guest_handler.py b/src/backend_pool/libvirt/guest_handler.py index f8cdfa49..8ca20849 100644 --- a/src/backend_pool/libvirt/guest_handler.py +++ b/src/backend_pool/libvirt/guest_handler.py @@ -19,18 +19,20 @@ def create_guest(connection, mac_address, guest_unique_id): import libvirt # get guest configurations - configuration_file = os.path.join( + configuration_file: str = os.path.join( CowrieConfig.get( "backend_pool", "config_files_path", fallback="share/pool_configs" ), CowrieConfig.get("backend_pool", "guest_config", fallback="default_guest.xml"), ) - version_tag = CowrieConfig.get("backend_pool", "guest_tag", fallback="guest") - base_image = CowrieConfig.get("backend_pool", "guest_image_path") - hypervisor = CowrieConfig.get("backend_pool", "guest_hypervisor", fallback="qemu") - memory = CowrieConfig.getint("backend_pool", "guest_memory", fallback=128) - qemu_machine = CowrieConfig.get( + version_tag: str = CowrieConfig.get("backend_pool", "guest_tag", fallback="guest") + base_image: str = CowrieConfig.get("backend_pool", "guest_image_path") + hypervisor: str = CowrieConfig.get( + "backend_pool", "guest_hypervisor", fallback="qemu" + ) + memory: int = CowrieConfig.getint("backend_pool", "guest_memory", fallback=128) + qemu_machine: str = CowrieConfig.get( "backend_pool", "guest_qemu_machine", fallback="pc-q35-3.1" ) @@ -44,19 +46,21 @@ def create_guest(connection, mac_address, guest_unique_id): os._exit(1) # only in some cases, like wrt - kernel_image = CowrieConfig.get("backend_pool", "guest_kernel_image", fallback="") + kernel_image: str = CowrieConfig.get( + "backend_pool", "guest_kernel_image", fallback="" + ) # get a directory to save snapshots, even if temporary try: # guest configuration, to be read by qemu, needs an absolute path - snapshot_path = backend_pool.util.to_absolute_path( + snapshot_path: str = backend_pool.util.to_absolute_path( CowrieConfig.get("backend_pool", "snapshot_path") ) except NoOptionError: snapshot_path = os.getcwd() # create a disk snapshot to be used by the guest - disk_img = os.path.join( + disk_img: str = os.path.join( snapshot_path, f"snapshot-{version_tag}-{guest_unique_id}.qcow2" ) diff --git a/src/backend_pool/libvirt/network_handler.py b/src/backend_pool/libvirt/network_handler.py index b334b04e..dbbc55a5 100644 --- a/src/backend_pool/libvirt/network_handler.py +++ b/src/backend_pool/libvirt/network_handler.py @@ -12,7 +12,7 @@ def create_filter(connection): # lazy import to avoid exception if not using the backend_pool and libvirt not installed (#1185) import libvirt - filter_file = os.path.join( + filter_file: str = os.path.join( CowrieConfig.get( "backend_pool", "config_files_path", fallback="share/pool_configs" ), @@ -39,7 +39,7 @@ def create_network(connection, network_table): import libvirt # TODO support more interfaces and therefore more IP space to allow > 253 guests - network_file = os.path.join( + network_file: str = os.path.join( CowrieConfig.get( "backend_pool", "config_files_path", fallback="share/pool_configs" ), @@ -50,8 +50,8 @@ def create_network(connection, network_table): network_xml = backend_pool.util.read_file(network_file) - template_host = "\n" - hosts = "" + template_host: str = "\n" + hosts: str = "" # generate a host entry for every possible guest in this network (253 entries) it = iter(network_table) diff --git a/src/backend_pool/nat.py b/src/backend_pool/nat.py index 2bc8271d..491a1720 100644 --- a/src/backend_pool/nat.py +++ b/src/backend_pool/nat.py @@ -51,9 +51,9 @@ class ServerProtocol(protocol.Protocol): class ServerFactory(protocol.Factory): - def __init__(self, dst_ip, dst_port): - self.dst_ip = dst_ip - self.dst_port = dst_port + def __init__(self, dst_ip: str, dst_port: int) -> None: + self.dst_ip: str = dst_ip + self.dst_port: int = dst_port def buildProtocol(self, addr): return ServerProtocol(self.dst_ip, self.dst_port) diff --git a/src/backend_pool/pool_server.py b/src/backend_pool/pool_server.py index 9236660d..ac8042e4 100644 --- a/src/backend_pool/pool_server.py +++ b/src/backend_pool/pool_server.py @@ -14,11 +14,15 @@ from cowrie.core.config import CowrieConfig class PoolServer(Protocol): def __init__(self, factory): self.factory = factory - self.local_pool = CowrieConfig.get("proxy", "pool", fallback="local") == "local" - self.pool_only = CowrieConfig.getboolean( + self.local_pool: bool = ( + CowrieConfig.get("proxy", "pool", fallback="local") == "local" + ) + self.pool_only: bool = CowrieConfig.getboolean( "backend_pool", "pool_only", fallback=False ) - self.use_nat = CowrieConfig.getboolean("backend_pool", "use_nat", fallback=True) + self.use_nat: bool = CowrieConfig.getboolean( + "backend_pool", "use_nat", fallback=True + ) if self.use_nat: self.nat_public_ip = CowrieConfig.get("backend_pool", "nat_public_ip") diff --git a/src/backend_pool/pool_service.py b/src/backend_pool/pool_service.py index 2d05ee0f..020456cf 100644 --- a/src/backend_pool/pool_service.py +++ b/src/backend_pool/pool_service.py @@ -22,7 +22,7 @@ class PoolService: VM States: created -> available -> using -> used -> unavailable -> destroyed - created: initialised but not fully booted by Qemu + created: initialised but not fully booted by QEMU available: can be requested using: a client is connected, can be served for other clients from same ip used: client disconnectec, but can still be served for its ip @@ -38,31 +38,35 @@ class PoolService: self.nat_service = nat_service self.guests = [] - self.guest_id = 0 + self.guest_id: int = 0 self.guest_lock = Lock() # time in seconds between each loop iteration - self.loop_sleep_time = 5 + self.loop_sleep_time: int = 5 self.loop_next_call = None # default configs; custom values will come from the client when they connect to the pool - self.max_vm = 2 - self.vm_unused_timeout = 600 - self.share_guests = True + self.max_vm: int = 2 + self.vm_unused_timeout: int = 600 + self.share_guests: bool = True # file configs - self.ssh_port = CowrieConfig.getint( + self.ssh_port: int = CowrieConfig.getint( "backend_pool", "guest_ssh_port", fallback=-1 ) - self.telnet_port = CowrieConfig.getint( + self.telnet_port: int = CowrieConfig.getint( "backend_pool", "guest_telnet_port", fallback=-1 ) - self.local_pool = CowrieConfig.get("proxy", "pool", fallback="local") == "local" - self.pool_only = CowrieConfig.getboolean( + self.local_pool: str = ( + CowrieConfig.get("proxy", "pool", fallback="local") == "local" + ) + self.pool_only: bool = CowrieConfig.getboolean( "backend_pool", "pool_only", fallback=False ) - self.use_nat = CowrieConfig.getboolean("backend_pool", "use_nat", fallback=True) + self.use_nat: bool = CowrieConfig.getboolean( + "backend_pool", "use_nat", fallback=True + ) # detect invalid config if not self.ssh_port > 0 and not self.telnet_port > 0: @@ -72,7 +76,7 @@ class PoolService: ) os._exit(1) - self.any_vm_up = False # TODO fix for no VM available + self.any_vm_up: bool = False # TODO fix for no VM available def start_pool(self): # cleanup older qemu objects @@ -124,7 +128,7 @@ class PoolService: try: self.qemu.stop_backend() except libvirt.libvirtError: - print("Not connected to Qemu") + print("Not connected to QEMU") def shutdown_pool(self): # lazy import to avoid exception if not using the backend_pool and libvirt not installed (#1185) @@ -135,7 +139,7 @@ class PoolService: try: self.qemu.shutdown_backend() except libvirt.libvirtError: - print("Not connected to Qemu") + print("Not connected to QEMU") def restart_pool(self): log.msg( @@ -188,7 +192,7 @@ class PoolService: return has_ssh or has_telnet # Producers - def __producer_mark_timed_out(self, guest_timeout): + def __producer_mark_timed_out(self, guest_timeout: int) -> None: """ Checks timed-out VMs and acquires lock to safely mark for deletion """ diff --git a/src/backend_pool/ssh_exec.py b/src/backend_pool/ssh_exec.py index 021bc525..b2e7f7fd 100644 --- a/src/backend_pool/ssh_exec.py +++ b/src/backend_pool/ssh_exec.py @@ -26,7 +26,7 @@ class CommandChannel(channel.SSHChannel): def channelOpen(self, data): self.conn.sendRequest(self, "exec", common.NS(self.command), wantReply=True) - def dataReceived(self, data): + def dataReceived(self, data: bytes) -> None: self.data += data def extReceived(self, dataType, data): diff --git a/src/backend_pool/telnet_exec.py b/src/backend_pool/telnet_exec.py index a222b1a9..30b6672a 100644 --- a/src/backend_pool/telnet_exec.py +++ b/src/backend_pool/telnet_exec.py @@ -13,20 +13,21 @@ class TelnetConnectionError(Exception): class TelnetClient(StatefulTelnetProtocol): def __init__(self): # output from server - self.response = b"" + self.response: bytes = b"" # callLater instance to wait until we have stop getting output for some time self.done_callback = None - def connectionMade(self): + self.command: bytes + def connectionMade(self): """ Set rawMode since we do not receive the login and password prompt in line mode. We return to default line mode when we detect the prompt in the received data stream. """ self.setRawMode() - def rawDataReceived(self, bytes): + def rawDataReceived(self, data): """ The login and password prompt on some systems are not received in lineMode. Therefore we do the authentication in raw mode and switch back to line mode @@ -38,17 +39,17 @@ class TelnetClient(StatefulTelnetProtocol): else: self.re_prompt = re.compile(self.factory.prompt.encode()) - if re.search(br"([Ll]ogin:\s+$)", bytes): + if re.search(br"([Ll]ogin:\s+$)", data): self.sendLine(self.factory.username.encode()) - elif re.search(br"([Pp]assword:\s+$)", bytes): + elif re.search(br"([Pp]assword:\s+$)", data): self.sendLine(self.factory.password.encode()) - elif self.re_prompt.search(bytes): + elif self.re_prompt.search(data): self.setLineMode() # auth is done, send command to server self.send_command(self.transport.factory.command) - def lineReceived(self, line): + def lineReceived(self, line: bytes) -> None: # ignore data sent by server before command is sent # ignore command echo from server if not self.command or line == self.command: @@ -62,11 +63,11 @@ class TelnetClient(StatefulTelnetProtocol): # start countdown to command done (when reached, consider the output was completely received and close) if not self.done_callback: - self.done_callback = reactor.callLater(0.5, self.close) + self.done_callback = reactor.callLater(0.5, self.close) # type: ignore else: self.done_callback.reset(0.5) - def send_command(self, command): + def send_command(self, command: str) -> None: """ Sends a command via Telnet using line mode """ @@ -113,13 +114,12 @@ class TelnetClientCommand: def __init__(self, callback, prompt, command): # callback to be called when execution is done self.callback = callback - self.prompt = prompt self.command = command def connect(self, host, port, username, password): # deferred to signal command and its output is done - done_deferred = defer.Deferred() + done_deferred: defer.Deferred = defer.Deferred() # start connection to the Telnet server factory = TelnetFactory( diff --git a/src/cowrie/commands/tar.py b/src/cowrie/commands/tar.py index 6e11559c..6df29fc4 100644 --- a/src/cowrie/commands/tar.py +++ b/src/cowrie/commands/tar.py @@ -18,8 +18,9 @@ class Command_tar(HoneyPotCommand): l, d = path.split("/"), [] while len(l): d.append(l.pop(0)) - if not self.fs.exists("/".join(d)): - self.fs.mkdir("/".join(d), 0, 0, 4096, f.mode, f.mtime) + p = "/".join(d) + if p and not self.fs.exists(p): + self.fs.mkdir(p, 0, 0, 4096, f.mode, f.mtime) def call(self): if len(self.args) < 2: diff --git a/src/cowrie/core/output.py b/src/cowrie/core/output.py index 34d25eb9..b4dbc693 100644 --- a/src/cowrie/core/output.py +++ b/src/cowrie/core/output.py @@ -68,12 +68,20 @@ def convert(input): """ This converts a nested dictionary with bytes in it to string """ + if isinstance(input, str): + return input + if isinstance(input, dict): + return {convert(key): convert(value) for key, value in list(input.items())} if isinstance(input, dict): return {convert(key): convert(value) for key, value in list(input.items())} elif isinstance(input, list): return [convert(element) for element in input] elif isinstance(input, bytes): - return input.decode("utf-8") + try: + string = input.decode("utf-8") + except UnicodeDecodeError: + string = repr(input) + return string else: return input diff --git a/src/cowrie/output/mysql.py b/src/cowrie/output/mysql.py index 4bcc508b..0e5f620a 100644 --- a/src/cowrie/output/mysql.py +++ b/src/cowrie/output/mysql.py @@ -65,7 +65,7 @@ class Output(cowrie.core.output.Output): use_unicode=True, ) except (MySQLdb.Error, MySQLdb._exceptions.Error) as e: - log.msg("output_mysql: Error %d: %s" % (e.args[0], e.args[1])) + log.msg(f"output_mysql: Error {e.args[0]}: {e.args[1]}") def stop(self): self.db.commit() @@ -76,18 +76,18 @@ class Output(cowrie.core.output.Output): 1146, "Table '...' doesn't exist" 1406, "Data too long for column '...' at row ..." """ - if error.value[0] in (1146, 1406): - log.msg(f"output_mysql: MySQL Error: {error.value}") + if error.value.args[0] in (1146, 1406): + log.msg(f"output_mysql: MySQL Error: {error.value.args!r}") log.msg("MySQL schema maybe misconfigured, doublecheck database!") else: - log.err(f"output_mysql: MySQL Error: {error.value}") + log.msg(f"output_mysql: MySQL Error: {error.value.args!r}") def simpleQuery(self, sql, args): """ Just run a deferred sql query, only care about errors """ if self.debug: - log.msg(f"output_mysql: MySQL query: {sql} {repr(args)}") + log.msg(f"output_mysql: MySQL query: {sql} {args!r}") d = self.db.runQuery(sql, args) d.addErrback(self.sqlerror) @@ -95,14 +95,14 @@ class Output(cowrie.core.output.Output): def write(self, entry): if entry["eventid"] == "cowrie.session.connect": r = yield self.db.runQuery( - "SELECT `id`" "FROM `sensors`" "WHERE `ip` = %s", (self.sensor,) + f"SELECT `id`\" \"FROM `sensors`\" \"WHERE `ip` = {self.sensor}" ) if r: sensorid = r[0][0] else: yield self.db.runQuery( - "INSERT INTO `sensors` (`ip`) " "VALUES (%s)", (self.sensor,) + f"INSERT INTO `sensors` (`ip`) \" \"VALUES ({self.sensor})" ) r = yield self.db.runQuery("SELECT LAST_INSERT_ID()") diff --git a/src/cowrie/pool_interface/client.py b/src/cowrie/pool_interface/client.py index bb762be8..b67d23de 100644 --- a/src/cowrie/pool_interface/client.py +++ b/src/cowrie/pool_interface/client.py @@ -11,7 +11,7 @@ from cowrie.core.config import CowrieConfig class PoolClient(Protocol): """ - Represents the connection between a protocol instance (SSH or Telnet) and a Qemu pool + Represents the connection between a protocol instance (SSH or Telnet) and a QEMU pool """ def __init__(self, factory): diff --git a/src/cowrie/ssh/forwarding.py b/src/cowrie/ssh/forwarding.py index e66d0d0b..9895761e 100644 --- a/src/cowrie/ssh/forwarding.py +++ b/src/cowrie/ssh/forwarding.py @@ -29,7 +29,9 @@ def cowrieOpenConnectForwardingClient(remoteWindow, remoteMaxPacket, data, avata ) # Forward redirect - redirectEnabled: bool = CowrieConfig.getboolean("ssh", "forward_redirect", fallback=False) + redirectEnabled: bool = CowrieConfig.getboolean( + "ssh", "forward_redirect", fallback=False + ) if redirectEnabled: redirects = {} items = CowrieConfig.items("ssh") @@ -56,7 +58,9 @@ def cowrieOpenConnectForwardingClient(remoteWindow, remoteMaxPacket, data, avata ) # TCP tunnel - tunnelEnabled: bool = CowrieConfig.getboolean("ssh", "forward_tunnel", fallback=False) + tunnelEnabled: bool = CowrieConfig.getboolean( + "ssh", "forward_tunnel", fallback=False + ) if tunnelEnabled: tunnels = {} items = CowrieConfig.items("ssh") diff --git a/src/cowrie/ssh_proxy/client_transport.py b/src/cowrie/ssh_proxy/client_transport.py index 8ace5194..9d3881d5 100644 --- a/src/cowrie/ssh_proxy/client_transport.py +++ b/src/cowrie/ssh_proxy/client_transport.py @@ -1,6 +1,8 @@ # Copyright (c) 2019 Guilherme Borges # All rights reserved. +from typing import Any + from twisted.conch.ssh import transport from twisted.internet import defer, protocol from twisted.protocols.policies import TimeoutMixin @@ -173,5 +175,8 @@ class BackendSSHTransport(transport.SSHClientTransport, TimeoutMixin): class BackendSSHFactory(protocol.ClientFactory): + + server: Any + def buildProtocol(self, addr): return BackendSSHTransport(self) diff --git a/src/cowrie/ssh_proxy/protocols/base_protocol.py b/src/cowrie/ssh_proxy/protocols/base_protocol.py index 0df33a9d..b2c78702 100644 --- a/src/cowrie/ssh_proxy/protocols/base_protocol.py +++ b/src/cowrie/ssh_proxy/protocols/base_protocol.py @@ -28,10 +28,10 @@ class BaseProtocol: - data = b"" - packetSize = 0 - name = "" - uuid = "" + data: bytes = b"" + packetSize: int = 0 + name: str = "" + uuid: str = "" ttylog_file = None def __init__(self, uuid=None, name=None, ssh=None): @@ -52,32 +52,32 @@ class BaseProtocol: def channel_closed(self): pass - def extract_int(self, length): + def extract_int(self, length: int) -> int: value = int.from_bytes(self.data[:length], byteorder="big") self.packetSize = self.packetSize - length self.data = self.data[length:] return value - def put_int(self, number): + def put_int(self, number: int) -> bytes: return number.to_bytes(4, byteorder="big") - def extract_string(self): + def extract_string(self) -> bytes: length = self.extract_int(4) value = self.data[:length] self.packetSize -= length self.data = self.data[length:] return value - def extract_bool(self): + def extract_bool(self) -> bool: value = self.extract_int(1) return bool(value) - def extract_data(self): + def extract_data(self) -> bytes: length = self.extract_int(4) self.packetSize = length value = self.data self.packetSize -= len(value) - self.data = "" + self.data = b"" return value def __deepcopy__(self, memo): diff --git a/src/cowrie/ssh_proxy/protocols/exec_term.py b/src/cowrie/ssh_proxy/protocols/exec_term.py index c20575d5..f7fe6cf1 100644 --- a/src/cowrie/ssh_proxy/protocols/exec_term.py +++ b/src/cowrie/ssh_proxy/protocols/exec_term.py @@ -52,12 +52,12 @@ class ExecTerm(base_protocol.BaseProtocol): self.transportId = ssh.server.transportId self.channelId = channelId - self.startTime = time.time() - self.ttylogPath = CowrieConfig.get("honeypot", "ttylog_path") - self.ttylogEnabled = CowrieConfig.getboolean( + self.startTime: float = time.time() + self.ttylogPath: str = CowrieConfig.get("honeypot", "ttylog_path") + self.ttylogEnabled: bool = CowrieConfig.getboolean( "honeypot", "ttylog", fallback=True ) - self.ttylogSize = 0 + self.ttylogSize: bool = 0 if self.ttylogEnabled: self.ttylogFile = "{}/{}-{}-{}e.log".format( diff --git a/src/cowrie/ssh_proxy/protocols/sftp.py b/src/cowrie/ssh_proxy/protocols/sftp.py index 7cc5c34a..e8ddf890 100644 --- a/src/cowrie/ssh_proxy/protocols/sftp.py +++ b/src/cowrie/ssh_proxy/protocols/sftp.py @@ -26,20 +26,22 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. +from typing import Optional + from twisted.python import log from cowrie.ssh_proxy.protocols import base_protocol class SFTP(base_protocol.BaseProtocol): - prevID = "" - ID = "" - handle = "" - path = "" - command = "" - payloadSize = 0 - payloadOffset = 0 - theFile = "" + prevID: str = "" + ID: str = "" + handle: str = "" + path: str = "" + command: str = "" + payloadSize: int = 0 + payloadOffset: int = 0 + theFile: str = "" packetLayout = { 1: "SSH_FXP_INIT", @@ -83,11 +85,11 @@ class SFTP(base_protocol.BaseProtocol): self.clientPacket = base_protocol.BaseProtocol() self.serverPacket = base_protocol.BaseProtocol() - self.parent = None + self.parent: Optional[str] = None self.parentPacket = None - self.offset = 0 + self.offset: int = 0 - def parse_packet(self, parent, payload): + def parse_packet(self, parent: str, payload: bytes) -> None: self.parent = parent if parent == "[SERVER]": @@ -99,7 +101,7 @@ class SFTP(base_protocol.BaseProtocol): self.parentPacket.packetSize = int(payload[:4].hex(), 16) - len(payload[4:]) payload = payload[4:] self.parentPacket.data = payload - payload = "" + payload = b"" else: if len(payload) > self.parentPacket.packetSize: @@ -111,7 +113,7 @@ class SFTP(base_protocol.BaseProtocol): else: self.parentPacket.packetSize -= len(payload) self.parentPacket.data = self.parentPacket.data + payload - payload = "" + payload = b"" if self.parentPacket.packetSize == 0: self.handle_packet(parent) @@ -120,11 +122,12 @@ class SFTP(base_protocol.BaseProtocol): self.parse_packet(parent, payload) def handle_packet(self, parent): - self.packetSize = self.parentPacket.packetSize - self.data = self.parentPacket.data + self.packetSize: int = self.parentPacket.packetSize + self.data: bytes = self.parentPacket.data + self.command: bytes - sftp_num = self.extract_int(1) - packet = self.packetLayout[sftp_num] + sftp_num: int = self.extract_int(1) + packet: str = self.packetLayout[sftp_num] self.prevID = self.ID self.ID = self.extract_int(4) @@ -252,8 +255,8 @@ class SFTP(base_protocol.BaseProtocol): ) def extract_attrs(self): - cmd = "" - flags = f"{self.extract_int(4):08b}" + cmd: bytes = "" + flags: str = f"{self.extract_int(4):08b}" if flags[5] == "1": perms = f"{self.extract_int(4):09b}" diff --git a/src/cowrie/ssh_proxy/protocols/term.py b/src/cowrie/ssh_proxy/protocols/term.py index 28a8af68..b1c78f37 100644 --- a/src/cowrie/ssh_proxy/protocols/term.py +++ b/src/cowrie/ssh_proxy/protocols/term.py @@ -40,20 +40,20 @@ class Term(base_protocol.BaseProtocol): def __init__(self, uuid, chan_name, ssh, channelId): super().__init__(uuid, chan_name, ssh) - self.command = b"" - self.pointer = 0 - self.tabPress = False - self.upArrow = False + self.command: bytes = b"" + self.pointer: int = 0 + self.tabPress: bool = False + self.upArrow: bool = False - self.transportId = ssh.server.transportId - self.channelId = channelId + self.transportId: int = ssh.server.transportId + self.channelId: int = channelId - self.startTime = time.time() - self.ttylogPath = CowrieConfig.get("honeypot", "ttylog_path") - self.ttylogEnabled = CowrieConfig.getboolean( + self.startTime: float = time.time() + self.ttylogPath: str = CowrieConfig.get("honeypot", "ttylog_path") + self.ttylogEnabled: bool = CowrieConfig.getboolean( "honeypot", "ttylog", fallback=True ) - self.ttylogSize = 0 + self.ttylogSize: int = 0 if self.ttylogEnabled: self.ttylogFile = "{}/{}-{}-{}i.log".format( @@ -61,7 +61,7 @@ class Term(base_protocol.BaseProtocol): ) ttylog.ttylog_open(self.ttylogFile, self.startTime) - def channel_closed(self): + def channel_closed(self) -> None: if self.ttylogEnabled: ttylog.ttylog_close(self.ttylogFile, time.time()) shasum = ttylog.ttylog_inputhash(self.ttylogFile) @@ -87,8 +87,8 @@ class Term(base_protocol.BaseProtocol): duration=time.time() - self.startTime, ) - def parse_packet(self, parent, payload): - self.data = payload + def parse_packet(self, parent: str, payload: bytes) -> None: + self.data: bytes = payload if parent == "[SERVER]": while len(self.data) > 0: @@ -196,7 +196,7 @@ class Term(base_protocol.BaseProtocol): elif self.data[:3] == b"\x1b\x5b\x43": self.pointer += 1 self.data = self.data[3:] - elif self.data[:2] == b"\x1b\x5b" and self.data[3] == b"\x50": + elif self.data[:2] == b"\x1b\x5b" and self.data[3:3] == b"\x50": self.data = self.data[4:] # Needed?! elif self.data[:1] != b"\x07" and self.data[:1] != b"\x0d":