2022-07-18 23:59:14 +00:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2023-10-07 08:37:45 +00:00
import paramiko
import re
import uuid
import logging
import time
2023-10-18 20:56:23 +00:00
import socket
2022-07-18 23:59:14 +00:00
2023-05-01 13:56:03 +00:00
from io import StringIO
2023-09-14 21:07:15 +00:00
from nxc . config import process_secret
2023-10-18 20:56:23 +00:00
from nxc . connection import connection , highlight
2023-09-14 21:07:15 +00:00
from nxc . logger import NXCAdapter
2023-05-02 15:17:59 +00:00
from paramiko . ssh_exception import (
AuthenticationException ,
NoValidConnectionsError ,
SSHException ,
)
2017-07-10 05:44:58 +00:00
2023-10-15 13:09:26 +00:00
2017-07-10 05:44:58 +00:00
class ssh ( connection ) :
2023-04-26 15:43:49 +00:00
def __init__ ( self , args , db , host ) :
2023-04-30 21:23:29 +00:00
self . protocol = " SSH "
2023-10-07 07:36:00 +00:00
self . remote_version = " Unknown SSH Version "
2023-09-18 08:26:23 +00:00
self . server_os_platform = " Linux "
self . user_principal = " root "
2023-04-30 21:23:29 +00:00
super ( ) . __init__ ( args , db , host )
2023-10-15 13:09:26 +00:00
2023-09-18 08:26:23 +00:00
def proto_flow ( self ) :
2023-10-18 20:56:23 +00:00
self . logger . debug ( " Kicking off proto_flow " )
2023-09-18 08:26:23 +00:00
self . proto_logger ( )
if self . create_conn_obj ( ) :
self . enum_host_info ( )
self . print_host_info ( )
2023-10-07 07:36:00 +00:00
if self . remote_version == " Unknown SSH Version " :
2023-09-18 08:26:23 +00:00
self . conn . close ( )
return
if self . login ( ) :
if hasattr ( self . args , " module " ) and self . args . module :
self . call_modules ( )
else :
self . call_cmd_args ( )
self . conn . close ( )
2023-04-26 15:43:49 +00:00
2017-07-10 05:44:58 +00:00
def proto_logger ( self ) :
2023-10-07 08:37:45 +00:00
logging . getLogger ( " paramiko " ) . disabled = True
logging . getLogger ( " paramiko.transport " ) . disabled = True
2023-09-14 21:07:15 +00:00
self . logger = NXCAdapter (
2023-03-30 03:59:22 +00:00
extra = {
2023-04-25 23:45:56 +00:00
" protocol " : " SSH " ,
" host " : self . host ,
" port " : self . args . port ,
2023-05-02 15:17:59 +00:00
" hostname " : self . hostname ,
2023-03-30 03:59:22 +00:00
}
)
2017-07-10 05:44:58 +00:00
def print_host_info ( self ) :
2023-10-07 07:36:00 +00:00
self . logger . display ( self . remote_version if self . remote_version != " Unknown SSH Version " else f " { self . remote_version } , skipping... " )
2021-11-17 12:37:14 +00:00
return True
2017-07-10 05:44:58 +00:00
def enum_host_info ( self ) :
2023-10-07 07:36:00 +00:00
if self . conn . _transport . remote_version :
self . remote_version = self . conn . _transport . remote_version
2023-10-21 15:13:47 +00:00
self . logger . debug ( f " Remote version: { self . remote_version } " )
2023-10-07 07:36:00 +00:00
self . db . add_host ( self . host , self . args . port , self . remote_version )
2017-07-10 05:44:58 +00:00
def create_conn_obj ( self ) :
self . conn = paramiko . SSHClient ( )
self . conn . set_missing_host_key_policy ( paramiko . AutoAddPolicy ( ) )
try :
2023-10-21 15:38:06 +00:00
self . conn . connect ( self . host , port = self . args . port , timeout = self . args . ssh_timeout , look_for_keys = False )
2017-07-10 05:44:58 +00:00
except AuthenticationException :
return True
except SSHException :
return True
except NoValidConnectionsError :
return False
2017-10-21 23:24:09 +00:00
except socket . error :
return False
2017-07-10 05:44:58 +00:00
def check_if_admin ( self ) :
2023-09-18 08:26:23 +00:00
self . admin_privs = False
if self . args . sudo_check :
self . check_if_admin_sudo ( )
return
2023-04-25 23:45:56 +00:00
# we could add in another method to check by piping in the password to sudo
# but that might be too much of an opsec concern - maybe add in a flag to do more checks?
2023-10-18 20:56:23 +00:00
self . logger . info ( " Determined user is root via `id; sudo -ln` command " )
2023-10-18 14:15:38 +00:00
_ , stdout , _ = self . conn . exec_command ( " id; sudo -ln 2>&1 " )
stdout = stdout . read ( ) . decode ( self . args . codec , errors = " ignore " )
2023-10-07 07:36:00 +00:00
admin_flag = {
2023-10-15 13:09:26 +00:00
" (root) " : [ True , None ] ,
2023-09-18 08:26:23 +00:00
" NOPASSWD: ALL " : [ True , None ] ,
" (ALL : ALL) ALL " : [ True , None ] ,
2023-10-21 15:13:47 +00:00
" (sudo) " : [ False , f " Current user: ' { self . username } ' was in ' sudo ' group, please try ' --sudo-check ' to check if user can run sudo shell " ]
2023-09-18 08:26:23 +00:00
}
2023-10-07 07:36:00 +00:00
for keyword in admin_flag . keys ( ) :
2023-09-18 08:26:23 +00:00
match = re . findall ( re . escape ( keyword ) , stdout )
if match :
2023-10-21 15:13:47 +00:00
self . logger . info ( f " User: ' { self . username } ' matched keyword: { match [ 0 ] } " )
2023-10-07 07:36:00 +00:00
self . admin_privs = admin_flag [ match [ 0 ] ] [ 0 ]
2023-10-07 08:37:45 +00:00
if not self . admin_privs :
2023-10-07 07:36:00 +00:00
tips = admin_flag [ match [ 0 ] ] [ 1 ]
2023-10-07 08:37:45 +00:00
else :
break
2023-09-18 08:26:23 +00:00
if not self . admin_privs and " tips " in locals ( ) :
self . logger . display ( tips )
return
def check_if_admin_sudo ( self ) :
if not self . password :
2023-10-20 07:56:39 +00:00
self . logger . error ( " Check admin with sudo does not support using a private key " )
2023-09-18 08:26:23 +00:00
return
2023-10-15 13:09:26 +00:00
2023-09-18 08:26:23 +00:00
if self . args . sudo_check_method :
method = self . args . sudo_check_method
self . logger . info ( f " Doing sudo check with method: { method } " )
2023-10-15 13:09:26 +00:00
2023-09-18 08:26:23 +00:00
if method == " sudo-stdin " :
2023-10-18 14:15:38 +00:00
_ , stdout , _ = self . conn . exec_command ( " sudo --help " )
stdout = stdout . read ( ) . decode ( self . args . codec , errors = " ignore " )
# Read sudo help docs and find "stdin"
2023-09-18 08:26:23 +00:00
if " stdin " in stdout :
2023-10-21 15:13:47 +00:00
shadow_backup = f " /tmp/ { uuid . uuid4 ( ) } "
2023-09-18 08:26:23 +00:00
# sudo support stdin password
2023-10-20 07:56:39 +00:00
self . conn . exec_command ( f " echo { self . password } | sudo -S cp /etc/shadow { shadow_backup } >/dev/null 2>&1 & " )
self . conn . exec_command ( f " echo { self . password } | sudo -S chmod 777 { shadow_backup } >/dev/null 2>&1 & " )
2023-09-18 08:26:23 +00:00
tries = 1
while True :
2023-10-20 07:56:39 +00:00
self . logger . info ( f " Checking { shadow_backup } if it existed " )
2023-10-21 15:13:47 +00:00
_ , _ , stderr = self . conn . exec_command ( f " ls { shadow_backup } " )
2023-09-18 08:26:23 +00:00
if tries > = self . args . get_output_tries :
2023-10-20 07:56:39 +00:00
self . logger . info ( f " The file { shadow_backup } does not exist, the pipe may be hanging. Increase the number of tries with the option ' --get-output-tries ' or change other method with ' --sudo-check-method ' . If it ' s still failing, maybe sudo shell does not work with the current user " )
2023-09-18 08:26:23 +00:00
break
2023-10-20 07:56:39 +00:00
if stderr . read ( ) . decode ( " utf-8 " ) :
2023-09-18 08:26:23 +00:00
time . sleep ( 2 )
2023-10-15 13:09:26 +00:00
tries + = 1
2023-09-18 08:26:23 +00:00
else :
2023-10-20 07:56:39 +00:00
self . logger . info ( f " { shadow_backup } existed " )
2023-09-18 08:26:23 +00:00
self . admin_privs = True
break
2023-10-21 11:08:36 +00:00
self . logger . info ( f " Remove up temporary files { shadow_backup } " )
self . conn . exec_command ( f " echo { self . password } | sudo -S rm -rf { shadow_backup } " )
2023-09-18 08:26:23 +00:00
else :
self . logger . error ( " Command: ' sudo ' not support stdin mode, running command with ' sudo ' failed " )
return
else :
2023-10-18 14:15:38 +00:00
_ , stdout , _ = self . conn . exec_command ( " mkfifo --help " )
stdout = stdout . read ( ) . decode ( self . args . codec , errors = " ignore " )
2023-09-18 08:26:23 +00:00
# check if user can execute mkfifo
if " Create named pipes " in stdout :
self . logger . info ( " Command: ' mkfifo ' available " )
2023-10-21 15:13:47 +00:00
pipe_stdin = f " /tmp/systemd- { uuid . uuid4 ( ) } "
pipe_stdout = f " /tmp/systemd- { uuid . uuid4 ( ) } "
shadow_backup = f " /tmp/ { uuid . uuid4 ( ) } "
2023-10-18 20:56:23 +00:00
self . conn . exec_command ( f " mkfifo { pipe_stdin } ; tail -f { pipe_stdin } | /bin/sh 2>&1 > { pipe_stdout } >/dev/null 2>&1 & " )
2023-09-18 08:26:23 +00:00
# 'script -qc /bin/sh /dev/null' means "upgrade" the shell, like reverse shell from netcat
2023-10-18 20:56:23 +00:00
self . conn . exec_command ( f " echo ' script -qc /bin/sh /dev/null ' > { pipe_stdin } " )
self . conn . exec_command ( f " echo ' sudo -s ' > { pipe_stdin } && echo ' { self . password } ' > { pipe_stdin } " )
2023-09-18 08:26:23 +00:00
# Sometime the pipe will hanging(only happen with paramiko)
# Can't get "whoami" or "id" result in pipe_stdout, maybe something wrong using pipe with paramiko
# But one thing I can confirm, is the command was executed even can't get result from pipe_stdout
tries = 1
2023-10-20 07:56:39 +00:00
self . logger . info ( f " Copy /etc/shadow to { shadow_backup } if pass the sudo auth " )
2023-09-18 08:26:23 +00:00
while True :
2023-10-20 07:56:39 +00:00
self . logger . info ( f " Checking { shadow_backup } if it existed " )
_ , _ , stderr = self . conn . exec_command ( f " ls { shadow_backup } " )
2023-09-18 08:26:23 +00:00
if tries > = self . args . get_output_tries :
2023-10-21 15:13:47 +00:00
self . logger . info ( f " The file { shadow_backup } does not exist, the pipe may be hanging. Increase the number of tries with the option ' --get-output-tries ' or change other method with ' --sudo-check-method ' . If it ' s still failing, maybe sudo shell does not work with the current user " )
2023-09-18 08:26:23 +00:00
break
2023-10-20 07:56:39 +00:00
if stderr . read ( ) . decode ( " utf-8 " ) :
2023-09-18 08:26:23 +00:00
time . sleep ( 2 )
2023-10-20 07:56:39 +00:00
self . conn . exec_command ( f " echo ' cp /etc/shadow { shadow_backup } && chmod 777 { shadow_backup } ' > { pipe_stdin } " )
2023-09-18 08:26:23 +00:00
tries + = 1
else :
2023-10-20 07:56:39 +00:00
self . logger . info ( f " { shadow_backup } existed " )
2023-09-18 08:26:23 +00:00
self . admin_privs = True
break
2023-10-21 11:08:36 +00:00
self . logger . info ( f " Remove up temporary files { shadow_backup } { pipe_stdin } { pipe_stdout } " )
self . conn . exec_command ( f " echo ' rm -rf { shadow_backup } ' > { pipe_stdin } && rm -rf { pipe_stdin } { pipe_stdout } " )
2023-09-18 08:26:23 +00:00
else :
self . logger . error ( " Command: ' mkfifo ' unavailable, running command with ' sudo ' failed " )
return
2017-07-10 05:44:58 +00:00
2023-04-30 21:23:29 +00:00
def plaintext_login ( self , username , password , private_key = None ) :
2023-09-18 08:26:23 +00:00
self . username = username
self . password = password
2023-10-07 15:35:36 +00:00
private_key = " "
2023-09-18 08:26:23 +00:00
stdout = None
2017-07-10 05:44:58 +00:00
try :
2023-05-01 01:49:45 +00:00
if self . args . key_file or private_key :
2023-10-18 20:56:23 +00:00
self . logger . debug ( " Logging in with key " )
2023-10-15 13:09:26 +00:00
2023-10-07 15:05:51 +00:00
if self . args . key_file :
2023-10-18 14:15:38 +00:00
with open ( self . args . key_file , " r " ) as f :
2023-10-07 15:05:51 +00:00
private_key = f . read ( )
pkey = paramiko . RSAKey . from_private_key ( StringIO ( private_key ) , password )
self . conn . _transport . auth_publickey ( username , pkey )
cred_id = self . db . add_credential (
" key " ,
username ,
password if password != " " else " " ,
key = private_key ,
)
2023-04-26 15:43:49 +00:00
2019-11-18 17:39:17 +00:00
else :
2023-09-18 08:26:23 +00:00
self . logger . debug ( f " Logging { self . host } with username: { self . username } , password: { self . password } " )
self . conn . _transport . auth_password ( username , password , fallback = True )
2023-10-07 08:37:45 +00:00
cred_id = self . db . add_credential ( " plaintext " , username , password )
2023-04-26 15:43:49 +00:00
2023-09-18 08:26:23 +00:00
# Some IOT devices will not raise exception in self.conn._transport.auth_password / self.conn._transport.auth_publickey
2023-10-18 14:15:38 +00:00
_ , stdout , _ = self . conn . exec_command ( " id " )
stdout = stdout . read ( ) . decode ( self . args . codec , errors = " ignore " )
2023-09-18 08:26:23 +00:00
except Exception as e :
2023-10-07 15:35:36 +00:00
if self . args . key_file :
password = f " { process_secret ( password ) } (keyfile: { self . args . key_file } ) "
self . logger . fail ( f " { username } : { password } { e } " )
2023-09-18 08:26:23 +00:00
self . conn . close ( )
return False
else :
2023-04-27 00:36:36 +00:00
shell_access = False
2023-04-26 15:43:49 +00:00
host_id = self . db . get_hosts ( self . host ) [ 0 ] . id
2017-07-10 05:44:58 +00:00
2023-09-18 08:26:23 +00:00
if not stdout :
2023-10-18 14:15:38 +00:00
_ , stdout , _ = self . conn . exec_command ( " whoami /priv " )
stdout = stdout . read ( ) . decode ( self . args . codec , errors = " ignore " )
2023-09-18 08:26:23 +00:00
self . server_os_platform = " Windows "
self . user_principal = " admin "
if " SeDebugPrivilege " in stdout :
self . admin_privs = True
elif " SeUndockPrivilege " in stdout :
self . admin_privs = True
self . user_principal = " admin (UAC) "
2023-04-26 15:43:49 +00:00
else :
2023-09-18 08:26:23 +00:00
# non admin (low priv)
self . user_principal = " admin (low priv) "
if not stdout :
self . logger . debug ( f " User: { self . username } can ' t get a basic shell " )
self . server_os_platform = " Network Devices "
shell_access = False
2023-04-27 00:36:36 +00:00
else :
2023-09-18 08:26:23 +00:00
shell_access = True
2023-10-07 08:37:45 +00:00
self . db . add_loggedin_relation ( cred_id , host_id , shell = shell_access )
2023-09-18 08:26:23 +00:00
if shell_access and self . server_os_platform == " Linux " :
self . check_if_admin ( )
if self . admin_privs :
self . logger . debug ( f " User { username } logged in successfully and is root! " )
if self . args . key_file :
self . db . add_admin_user ( " key " , username , password , host_id = host_id , cred_id = cred_id )
else :
self . db . add_admin_user (
" plaintext " ,
username ,
password ,
host_id = host_id ,
cred_id = cred_id ,
)
2023-04-26 15:43:49 +00:00
if self . args . key_file :
2023-10-07 15:35:36 +00:00
password = f " { process_secret ( password ) } (keyfile: { self . args . key_file } ) "
2023-04-26 15:43:49 +00:00
2023-10-07 16:02:08 +00:00
display_shell_access = " {} {} {} " . format (
2023-10-07 08:37:45 +00:00
f " ( { self . user_principal } ) " if self . admin_privs else f " (non { self . user_principal } ) " ,
self . server_os_platform ,
2023-10-21 15:13:47 +00:00
" - Shell access! " if shell_access else " "
2023-10-07 08:37:45 +00:00
)
2023-10-07 16:02:08 +00:00
2023-10-07 15:35:36 +00:00
self . logger . success ( f " { username } : { password } { self . mark_pwned ( ) } { highlight ( display_shell_access ) } " )
2023-10-15 13:09:26 +00:00
2023-04-23 11:45:16 +00:00
return True
2023-10-15 13:09:26 +00:00
2023-09-18 08:26:23 +00:00
def execute ( self , payload = None , get_output = False ) :
if not payload and self . args . execute :
payload = self . args . execute
if not self . args . no_output :
get_output = True
2020-04-28 10:11:16 +00:00
try :
2023-10-18 14:15:38 +00:00
_ , stdout , _ = self . conn . exec_command ( f " { payload } 2>&1 " )
stdout = stdout . read ( ) . decode ( self . args . codec , errors = " ignore " )
except Exception as e :
self . logger . fail ( f " Execute command failed, error: { str ( e ) } " )
return False
else :
2023-04-26 15:43:49 +00:00
self . logger . success ( " Executed command " )
2023-09-18 08:26:23 +00:00
if get_output :
2023-10-21 15:13:47 +00:00
for line in stdout . split ( " \n " ) :
self . logger . highlight ( line . strip ( " \n " ) )
2023-10-18 14:15:38 +00:00
return stdout