1# Copyright (c) 2016 Thomas Nicholson <tnnich@googlemail.com> 2# All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without 5# modification, are permitted provided that the following conditions 6# are met: 7# 8# 1. Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# 2. Redistributions in binary form must reproduce the above copyright 11# notice, this list of conditions and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# 3. The names of the author(s) may not be used to endorse or promote 14# products derived from this software without specific prior written 15# permission. 16# 17# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 18# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 19# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 20# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 22# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 24# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27# SUCH DAMAGE. 28 29import uuid 30 31from twisted.python import log 32 33from cowrie.core.config import CowrieConfig 34from cowrie.ssh_proxy.protocols import base_protocol, exec_term, port_forward, sftp, term 35from cowrie.ssh_proxy.util import int_to_hex, string_to_hex 36 37 38class SSH(base_protocol.BaseProtocol): 39 packetLayout = { 40 1: 'SSH_MSG_DISCONNECT', # ['uint32', 'reason_code'], ['string', 'reason'], ['string', 'language_tag'] 41 2: 'SSH_MSG_IGNORE', # ['string', 'data'] 42 3: 'SSH_MSG_UNIMPLEMENTED', # ['uint32', 'seq_no'] 43 4: 'SSH_MSG_DEBUG', # ['boolean', 'always_display'] 44 5: 'SSH_MSG_SERVICE_REQUEST', # ['string', 'service_name'] 45 6: 'SSH_MSG_SERVICE_ACCEPT', # ['string', 'service_name'] 46 20: 'SSH_MSG_KEXINIT', # ['string', 'service_name'] 47 21: 'SSH_MSG_NEWKEYS', 48 50: 'SSH_MSG_USERAUTH_REQUEST', # ['string', 'username'], ['string', 'service_name'], ['string', 'method_name'] 49 51: 'SSH_MSG_USERAUTH_FAILURE', # ['name-list', 'authentications'], ['boolean', 'partial_success'] 50 52: 'SSH_MSG_USERAUTH_SUCCESS', # 51 53: 'SSH_MSG_USERAUTH_BANNER', # ['string', 'message'], ['string', 'language_tag'] 52 60: 'SSH_MSG_USERAUTH_INFO_REQUEST', # ['string', 'name'], ['string', 'instruction'], 53 # ['string', 'language_tag'], ['uint32', 'num-prompts'], 54 # ['string', 'prompt[x]'], ['boolean', 'echo[x]'] 55 61: 'SSH_MSG_USERAUTH_INFO_RESPONSE', # ['uint32', 'num-responses'], ['string', 'response[x]'] 56 80: 'SSH_MSG_GLOBAL_REQUEST', # ['string', 'request_name'], ['boolean', 'want_reply'] #tcpip-forward 57 81: 'SSH_MSG_REQUEST_SUCCESS', 58 82: 'SSH_MSG_REQUEST_FAILURE', 59 90: 'SSH_MSG_CHANNEL_OPEN', # ['string', 'channel_type'], ['uint32', 'sender_channel'], 60 # ['uint32', 'initial_window_size'], ['uint32', 'maximum_packet_size'], 61 91: 'SSH_MSG_CHANNEL_OPEN_CONFIRMATION', # ['uint32', 'recipient_channel'], ['uint32', 'sender_channel'], 62 # ['uint32', 'initial_window_size'], ['uint32', 'maximum_packet_size'] 63 92: 'SSH_MSG_CHANNEL_OPEN_FAILURE', # ['uint32', 'recipient_channel'], ['uint32', 'reason_code'], 64 # ['string', 'reason'], ['string', 'language_tag'] 65 93: 'SSH_MSG_CHANNEL_WINDOW_ADJUST', # ['uint32', 'recipient_channel'], ['uint32', 'additional_bytes'] 66 94: 'SSH_MSG_CHANNEL_DATA', # ['uint32', 'recipient_channel'], ['string', 'data'] 67 95: 'SSH_MSG_CHANNEL_EXTENDED_DATA', # ['uint32', 'recipient_channel'], 68 # ['uint32', 'data_type_code'], ['string', 'data'] 69 96: 'SSH_MSG_CHANNEL_EOF', # ['uint32', 'recipient_channel'] 70 97: 'SSH_MSG_CHANNEL_CLOSE', # ['uint32', 'recipient_channel'] 71 98: 'SSH_MSG_CHANNEL_REQUEST', # ['uint32', 'recipient_channel'], ['string', 'request_type'], 72 # ['boolean', 'want_reply'] 73 99: 'SSH_MSG_CHANNEL_SUCCESS', 74 100: 'SSH_MSG_CHANNEL_FAILURE' 75 } 76 77 def __init__(self, server): 78 super(SSH, self).__init__() 79 80 self.channels = [] 81 self.username = '' 82 self.password = '' 83 self.auth_type = '' 84 85 self.sendOn = False 86 self.expect_password = 0 87 self.server = server 88 self.channels = [] 89 self.client = None 90 91 def set_client(self, client): 92 self.client = client 93 94 def parse_packet(self, parent, message_num, payload): 95 self.data = payload 96 self.packetSize = len(payload) 97 self.sendOn = True 98 99 if message_num in self.packetLayout: 100 packet = self.packetLayout[message_num] 101 else: 102 packet = 'UNKNOWN_{0}'.format(message_num) 103 104 if parent == '[SERVER]': 105 direction = 'PROXY -> BACKEND' 106 else: 107 direction = 'BACKEND -> PROXY' 108 109 # log raw packets if user sets so 110 if CowrieConfig().getboolean('proxy', 'log_raw', fallback=False): 111 log.msg( 112 eventid='cowrie.proxy.ssh', 113 format="%(direction)s - %(packet)s - %(payload)s", 114 direction=direction, 115 packet=packet.ljust(37), 116 payload=repr(payload), 117 protocol='ssh' 118 ) 119 120 if packet == 'SSH_MSG_SERVICE_REQUEST': 121 service = self.extract_string() 122 if service == b'ssh-userauth': 123 self.sendOn = False 124 125 # - UserAuth 126 if packet == 'SSH_MSG_USERAUTH_REQUEST': 127 self.sendOn = False 128 self.username = self.extract_string() 129 self.extract_string() # service 130 self.auth_type = self.extract_string() 131 132 if self.auth_type == b'password': 133 self.extract_bool() 134 self.password = self.extract_string() 135 # self.server.sendPacket(52, b'') 136 137 elif self.auth_type == b'publickey': 138 self.sendOn = False 139 self.server.sendPacket(51, string_to_hex('password') + chr(0).encode()) 140 141 elif packet == 'SSH_MSG_USERAUTH_FAILURE': 142 self.sendOn = False 143 auth_list = self.extract_string() 144 145 if b'publickey' in auth_list: 146 log.msg('[SSH] Detected Public Key Auth - Disabling!') 147 payload = string_to_hex('password') + chr(0).encode() 148 149 elif packet == 'SSH_MSG_USERAUTH_SUCCESS': 150 self.sendOn = False 151 152 elif packet == 'SSH_MSG_USERAUTH_INFO_REQUEST': 153 self.sendOn = False 154 self.auth_type = b'keyboard-interactive' 155 self.extract_string() 156 self.extract_string() 157 self.extract_string() 158 num_prompts = self.extract_int(4) 159 for i in range(0, num_prompts): 160 request = self.extract_string() 161 self.extract_bool() 162 163 if 'password' in request.lower(): 164 self.expect_password = i 165 166 elif packet == 'SSH_MSG_USERAUTH_INFO_RESPONSE': 167 self.sendOn = False 168 num_responses = self.extract_int(4) 169 for i in range(0, num_responses): 170 response = self.extract_string() 171 if i == self.expect_password: 172 self.password = response 173 174 # - End UserAuth 175 # - Channels 176 elif packet == 'SSH_MSG_CHANNEL_OPEN': 177 channel_type = self.extract_string() 178 channel_id = self.extract_int(4) 179 180 log.msg('got channel {} request'.format(channel_type)) 181 182 if channel_type == b'session': 183 # if using an interactive session reset frontend timeout 184 self.server.setTimeout(CowrieConfig().getint('honeypot', 'interactive_timeout', fallback=300)) 185 186 self.create_channel(parent, channel_id, channel_type) 187 188 elif channel_type == b'direct-tcpip' or channel_type == b'forwarded-tcpip': 189 self.extract_int(4) 190 self.extract_int(4) 191 192 dst_ip = self.extract_string() 193 dst_port = self.extract_int(4) 194 195 src_ip = self.extract_string() 196 src_port = self.extract_int(4) 197 198 if CowrieConfig().getboolean('ssh', 'forwarding'): 199 log.msg(eventid='cowrie.direct-tcpip.request', 200 format='direct-tcp connection request to %(dst_ip)s:%(dst_port)s ' 201 'from %(src_ip)s:%(src_port)s', 202 dst_ip=dst_ip, dst_port=dst_port, 203 src_ip=src_ip, src_port=src_port) 204 205 the_uuid = uuid.uuid4().hex 206 self.create_channel(parent, channel_id, channel_type) 207 208 if parent == '[SERVER]': 209 other_parent = '[CLIENT]' 210 the_name = '[LPRTF' + str(channel_id) + ']' 211 else: 212 other_parent = '[SERVER]' 213 the_name = '[RPRTF' + str(channel_id) + ']' 214 215 channel = self.get_channel(channel_id, other_parent) 216 channel['name'] = the_name 217 channel['session'] = port_forward.PortForward(the_uuid, channel['name'], self) 218 219 else: 220 log.msg('[SSH] Detected Port Forwarding Channel - Disabling!') 221 log.msg(eventid='cowrie.direct-tcpip.data', 222 format='discarded direct-tcp forward request %(id)s to %(dst_ip)s:%(dst_port)s ', 223 dst_ip=dst_ip, dst_port=dst_port) 224 225 self.sendOn = False 226 self.send_back(parent, 92, int_to_hex(channel_id) + int_to_hex(1) + 227 string_to_hex('open failed') + int_to_hex(0)) 228 else: 229 # UNKNOWN CHANNEL TYPE 230 if channel_type not in [b'exit-status']: 231 log.msg('[SSH Unknown Channel Type Detected - {0}'.format(channel_type)) 232 233 elif packet == 'SSH_MSG_CHANNEL_OPEN_CONFIRMATION': 234 channel = self.get_channel(self.extract_int(4), parent) 235 # SENDER 236 sender_id = self.extract_int(4) 237 238 if parent == '[SERVER]': 239 channel['serverID'] = sender_id 240 elif parent == '[CLIENT]': 241 channel['clientID'] = sender_id 242 # CHANNEL OPENED 243 244 elif packet == 'SSH_MSG_CHANNEL_OPEN_FAILURE': 245 channel = self.get_channel(self.extract_int(4), parent) 246 self.channels.remove(channel) 247 # CHANNEL FAILED TO OPEN 248 249 elif packet == 'SSH_MSG_CHANNEL_REQUEST': 250 channel = self.get_channel(self.extract_int(4), parent) 251 channel_type = self.extract_string() 252 the_uuid = uuid.uuid4().hex 253 254 if channel_type == b'shell': 255 channel['name'] = '[TERM' + str(channel['serverID']) + ']' 256 channel['session'] = term.Term(the_uuid, channel['name'], self, channel['clientID']) 257 258 elif channel_type == b'exec': 259 channel['name'] = '[EXEC' + str(channel['serverID']) + ']' 260 self.extract_bool() 261 command = self.extract_string() 262 channel['session'] = exec_term.ExecTerm(the_uuid, channel['name'], self, channel['serverID'], command) 263 264 elif channel_type == b'subsystem': 265 self.extract_bool() 266 subsystem = self.extract_string() 267 268 if subsystem == b'sftp': 269 if CowrieConfig().getboolean('ssh', 'sftp_enabled'): 270 channel['name'] = '[SFTP' + str(channel['serverID']) + ']' 271 # self.out.channel_opened(the_uuid, channel['name']) 272 channel['session'] = sftp.SFTP(the_uuid, channel['name'], self) 273 else: 274 # log.msg(log.LPURPLE, '[SSH]', 'Detected SFTP Channel Request - Disabling!') 275 self.sendOn = False 276 self.send_back(parent, 100, int_to_hex(channel['serverID'])) 277 else: 278 # UNKNOWN SUBSYSTEM 279 log.msg('[SSH] Unknown Subsystem Type Detected - ' + subsystem.decode()) 280 else: 281 # UNKNOWN CHANNEL REQUEST TYPE 282 if channel_type not in [b'window-change', b'env', b'pty-req', b'exit-status', b'exit-signal']: 283 log.msg('[SSH] Unknown Channel Request Type Detected - {0}'.format(channel_type.decode())) 284 285 elif packet == 'SSH_MSG_CHANNEL_FAILURE': 286 pass 287 288 elif packet == 'SSH_MSG_CHANNEL_CLOSE': 289 channel = self.get_channel(self.extract_int(4), parent) 290 # Is this needed?! 291 channel[parent] = True 292 293 if '[SERVER]' in channel and '[CLIENT]' in channel: 294 # CHANNEL CLOSED 295 if channel['session'] is not None: 296 log.msg('remote close') 297 channel['session'].channel_closed() 298 299 self.channels.remove(channel) 300 # - END Channels 301 # - ChannelData 302 elif packet == 'SSH_MSG_CHANNEL_DATA': 303 channel = self.get_channel(self.extract_int(4), parent) 304 channel['session'].parse_packet(parent, self.extract_string()) 305 306 elif packet == 'SSH_MSG_CHANNEL_EXTENDED_DATA': 307 channel = self.get_channel(self.extract_int(4), parent) 308 self.extract_int(4) 309 channel['session'].parse_packet(parent, self.extract_string()) 310 # - END ChannelData 311 312 elif packet == 'SSH_MSG_GLOBAL_REQUEST': 313 channel_type = self.extract_string() 314 if channel_type == b'tcpip-forward': 315 if not CowrieConfig().getboolean(['ssh', 'forwarding']): 316 self.sendOn = False 317 self.send_back(parent, 82, '') 318 319 if self.sendOn: 320 if parent == '[SERVER]': 321 self.client.sendPacket(message_num, payload) 322 else: 323 self.server.sendPacket(message_num, payload) 324 325 def send_back(self, parent, message_num, payload): 326 packet = self.packetLayout[message_num] 327 328 if parent == '[SERVER]': 329 direction = 'PROXY -> FRONTEND' 330 else: 331 direction = 'PROXY -> BACKEND' 332 333 log.msg( 334 eventid='cowrie.proxy.ssh', 335 format="%(direction)s - %(packet)s - %(payload)s", 336 direction=direction, 337 packet=packet.ljust(37), 338 payload=repr(payload), 339 protocol='ssh' 340 ) 341 342 if parent == '[SERVER]': 343 self.server.sendPacket(message_num, payload) 344 elif parent == '[CLIENT]': 345 self.client.sendPacket(message_num, payload) 346 347 def create_channel(self, parent, channel_id, channel_type, session=None): 348 if parent == '[SERVER]': 349 self.channels.append({'serverID': channel_id, 'type': channel_type, 'session': session}) 350 elif parent == '[CLIENT]': 351 self.channels.append({'clientID': channel_id, 'type': channel_type, 'session': session}) 352 353 def get_channel(self, channel_num, parent): 354 the_channel = None 355 for channel in self.channels: 356 if parent == '[CLIENT]': 357 search = 'serverID' 358 else: 359 search = 'clientID' 360 361 if channel[search] == channel_num: 362 the_channel = channel 363 break 364 return the_channel 365