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