Merge pull request #136 from XiaoliChan/mssql-improv

[MSSQL] Improvement
main
Marshall Hallenbeck 2024-03-09 01:52:00 -05:00 committed by GitHub
commit d9e46dbb1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 214 additions and 253 deletions

View File

@ -98,7 +98,7 @@ class NXCModule:
with open(os.path.join(self.nano_path, self.nano), "rb") as nano:
try:
self.context.log.display(f"Copy {self.nano} to {self.remote_tmp_dir}")
exec_method = MSSQLEXEC(self.connection.conn)
exec_method = MSSQLEXEC(self.connection.conn, self.context.log)
exec_method.put_file(nano.read(), self.remote_tmp_dir + self.nano)
if exec_method.file_exists(self.remote_tmp_dir + self.nano):
self.context.log.success(f"Created file {self.nano} on the remote machine {self.remote_tmp_dir}")
@ -118,13 +118,13 @@ class NXCModule:
self.context.log.display(f"Getting LSASS PID via command {command}")
p = self.connection.execute(command, display_output)
self.context.log.debug(f"tasklist Command Result: {p}")
if len(p) == 1:
p = p[0]
if not p or p == "None":
self.context.log.fail("Failed to execute command to get LSASS PID")
return
if len(p) == 1:
p = p[0]
pid = p.split(",")[1][1:-1]
self.context.log.debug(f"pid: {pid}")
timestamp = datetime.today().strftime("%Y%m%d_%H%M")

View File

@ -1,15 +1,19 @@
import os
import random
import socket
import contextlib
from nxc.config import process_secret
from nxc.connection import connection
from nxc.connection import requires_admin
from nxc.logger import NXCAdapter
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
from nxc.helpers.bloodhound import add_user_bh
from nxc.helpers.ntlm_parser import parse_challenge
from nxc.helpers.powershell import create_ps_command
from impacket import tds
from nxc.protocols.mssql.mssqlexec import MSSQLEXEC
from impacket import tds, ntlm
from impacket.krb5.ccache import CCache
from impacket.smbconnection import SMBConnection, SessionError
from impacket.tds import (
SQLErrorException,
TDS_LOGINACK_TOKEN,
@ -22,31 +26,20 @@ from impacket.tds import (
TDS_ENVCHANGE_CHARSET,
TDS_ENVCHANGE_PACKETSIZE,
)
import contextlib
class mssql(connection):
def __init__(self, args, db, host):
self.mssql_instances = None
self.mssql_instances = []
self.domain = None
self.server_os = None
self.hash = None
self.os_arch = None
self.nthash = ""
self.is_mssql = False
connection.__init__(self, args, db, host)
def proto_flow(self):
self.proto_logger()
if self.create_conn_obj():
self.enum_host_info()
self.print_host_info()
if self.login():
if hasattr(self.args, "module") and self.args.module:
self.call_modules()
else:
self.call_cmd_args()
def proto_logger(self):
self.logger = NXCAdapter(
extra={
@ -57,83 +50,98 @@ class mssql(connection):
}
)
def enum_host_info(self):
# this try pass breaks module http server, more info https://github.com/byt3bl33d3r/CrackMapExec/issues/363
try: # noqa: SIM105
# Probably a better way of doing this, grab our IP from the socket
self.local_ip = str(self.conn.socket).split()[2].split("=")[1].split(":")[0]
except Exception:
pass
if self.args.no_smb:
self.domain = self.args.domain
def create_conn_obj(self):
try:
self.conn = tds.MSSQL(self.host, self.port)
# Default has not timeout option in tds.MSSQL.connect() function, let rewrite it.
af, socktype, proto, canonname, sa = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM)[0]
sock = socket.socket(af, socktype, proto)
sock.settimeout(self.args.mssql_timeout)
sock.connect(sa)
self.conn.socket = sock
if not self.is_mssql:
self.conn.preLogin()
except Exception as e:
self.logger.debug(f"Error connecting to MSSQL service on host: {self.host}, reason: {e}")
return False
else:
try:
smb_conn = SMBConnection(self.host, self.host, None)
try:
smb_conn.login("", "")
except SessionError as e:
if "STATUS_ACCESS_DENIED" in e.getErrorString():
pass
self.domain = smb_conn.getServerDNSDomainName()
self.hostname = smb_conn.getServerName()
self.server_os = smb_conn.getServerOS()
self.logger.extra["hostname"] = self.hostname
self.is_mssql = True
return True
def reconnect_mssql(func):
def wrapper(self, *args, **kwargs):
with contextlib.suppress(Exception):
smb_conn.logoff()
self.conn.disconnect()
# When using ccache file, we must need to set target host to hostname when creating connection object.
if self.kerberos:
self.host = self.hostname
self.create_conn_obj()
return func(self, *args, **kwargs)
return wrapper
def check_if_admin(self):
self.admin_privs = False
try:
results = self.conn.sql_query("SELECT IS_SRVROLEMEMBER('sysadmin')")
is_admin = int(results[0][""])
except Exception as e:
self.logger.fail(f"Error querying for sysadmin role: {e}")
else:
if is_admin:
self.admin_privs = True
@reconnect_mssql
def enum_host_info(self):
challenge = None
try:
login = tds.TDS_LOGIN()
login["HostName"] = ""
login["AppName"] = ""
login["ServerName"] = self.conn.server.encode("utf-16le")
login["CltIntName"] = login["AppName"]
login["ClientPID"] = random.randint(0, 1024)
login["PacketSize"] = self.conn.packetSize
login["OptionFlags2"] = tds.TDS_INIT_LANG_FATAL | tds.TDS_ODBC_ON | tds.TDS_INTEGRATED_SECURITY_ON
# NTLMSSP Negotiate
auth = ntlm.getNTLMSSPType1("", "")
login["SSPI"] = auth.getData()
login["Length"] = len(login.getData())
# Get number of mssql instance
self.mssql_instances = self.conn.getInstances(0)
# Send the NTLMSSP Negotiate or SQL Auth Packet
self.conn.sendTDS(tds.TDS_LOGIN7, login.getData())
tdsx = self.conn.recvTDS()
challenge = tdsx["Data"][3:]
self.logger.info(f"NTLM challenge: {challenge!s}")
except Exception as e:
self.logger.info(f"Failed to receive NTLM challenge, reason: {e!s}")
return False
else:
ntlm_info = parse_challenge(challenge)
self.domain = ntlm_info["domain"]
self.hostname = ntlm_info["hostname"]
self.server_os = ntlm_info["os_version"]
self.logger.extra["hostname"] = self.hostname
self.db.add_host(self.host, self.hostname, self.domain, self.server_os, len(self.mssql_instances),)
if self.args.domain:
self.domain = self.args.domain
if self.args.local_auth:
self.domain = self.hostname
except Exception as e:
self.logger.fail(f"Error retrieving host domain: {e} specify one manually with the '-d' flag")
self.mssql_instances = self.conn.getInstances(0)
self.db.add_host(
self.host,
self.hostname,
self.domain,
self.server_os,
len(self.mssql_instances),
)
with contextlib.suppress(Exception):
self.conn.disconnect()
if self.domain is None:
self.domain = ""
def print_host_info(self):
self.logger.display(f"{self.server_os} (name:{self.hostname}) (domain:{self.domain})")
# if len(self.mssql_instances) > 0:
# for i, instance in enumerate(self.mssql_instances):
# for key in instance.keys():
def create_conn_obj(self):
try:
self.conn = tds.MSSQL(self.host, self.port)
self.conn.connect()
except OSError as e:
self.logger.debug(f"Error connecting to MSSQL: {e}")
return False
return True
def check_if_admin(self):
try:
results = self.conn.sql_query("SELECT IS_SRVROLEMEMBER('sysadmin')")
is_admin = int(results[0][""])
except Exception as e:
self.logger.fail(f"Error querying for sysadmin role: {e}")
return False
if is_admin:
self.admin_privs = True
self.logger.debug("User is admin")
else:
return False
return True
@reconnect_mssql
def kerberos_login(
self,
domain,
@ -144,146 +152,126 @@ class mssql(connection):
kdcHost="",
useCache=False,
):
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
self.username = username
self.password = password
self.domain = domain
self.nthash = ""
hashes = None
if ntlm_hash != "":
if ntlm_hash:
if ntlm_hash.find(":") != -1:
hashes = ntlm_hash
ntlm_hash.split(":")[1]
self.nthash = ntlm_hash.split(":")[1]
hashes = f":{self.nthash}"
else:
# only nt hash
hashes = f":{ntlm_hash}"
self.nthash = ntlm_hash
hashes = f":{self.nthash}"
kerb_pass = next(s for s in [self.nthash, password, aesKey] if s) if not all(s == "" for s in [self.nthash, password, aesKey]) else ""
if useCache and kerb_pass == "":
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
username = ccache.credentials[0].header["client"].prettyPrint().decode().split("@")[0]
self.username = username
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
try:
res = self.conn.kerberosLogin(
None,
username,
password,
domain,
self.username,
self.password,
self.domain,
hashes,
aesKey,
kdcHost=kdcHost,
useCache=useCache,
)
if res is not True:
self.conn.printReplies()
return False
self.password = password
if username == "" and useCache:
ccache = CCache.loadFile(os.getenv("KRB5CCNAME"))
principal = ccache.principal.toPrincipal()
self.username = principal.components[0]
username = principal.components[0]
else:
self.username = username
self.domain = domain
raise
self.check_if_admin()
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
domain = f"{domain}\\" if not self.args.local_auth else ""
self.logger.success(f"{domain}{username}{used_ccache} {self.mark_pwned()}")
self.logger.success(f"{self.domain}\\{self.username}{used_ccache} {self.mark_pwned()}")
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
return True
except Exception as e:
used_ccache = " from ccache" if useCache else f":{process_secret(kerb_pass)}"
domain = f"{domain}\\" if not self.args.local_auth else ""
self.logger.fail(f"{domain}\\{username}{used_ccache} {e}")
return False
def plaintext_login(self, domain, username, password):
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
try:
# this is to prevent a decoding issue in impacket/ntlm.py:617 where it attempts to decode the domain
if not domain:
domain = ""
res = self.conn.login(None, username, password, domain, None, not self.args.local_auth)
if res is not True:
self.handle_mssql_reply()
return False
self.password = password
self.username = username
self.domain = domain
self.check_if_admin()
self.db.add_credential("plaintext", domain, username, password)
if self.admin_privs:
self.db.add_admin_user("plaintext", domain, username, password, self.host)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
domain = f"{domain}\\" if not self.args.local_auth else ""
out = f"{domain}{username}:{process_secret(password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
return True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
return False
except Exception as e:
self.logger.fail(f"{domain}\\{username}:{process_secret(password)}")
self.logger.exception(e)
except Exception:
error_msg = self.handle_mssql_reply()
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, kerb_pass, error_msg if error_msg else ""))
return False
def hash_login(self, domain, username, ntlm_hash):
lmhash = ""
nthash = ""
# This checks to see if we didn't provide the LM Hash
if ntlm_hash.find(":") != -1:
lmhash, nthash = ntlm_hash.split(":")
else:
nthash = ntlm_hash
with contextlib.suppress(Exception):
self.conn.disconnect()
self.create_conn_obj()
@reconnect_mssql
def plaintext_login(self, domain, username, password):
self.password = password
self.username = username
self.domain = domain
try:
res = self.conn.login(
None,
username,
"",
domain,
":" + nthash if not lmhash else ntlm_hash,
self.username,
self.password,
self.domain,
None,
not self.args.local_auth,
)
if res is not True:
self.conn.printReplies()
return False
self.hash = ntlm_hash
self.username = username
self.domain = domain
raise
self.check_if_admin()
self.db.add_credential("hash", domain, username, ntlm_hash)
if self.admin_privs:
self.db.add_admin_user("hash", domain, username, ntlm_hash, self.host)
add_user_bh(f"{self.hostname}$", domain, self.logger, self.config)
out = f"{domain}\\{username} {process_secret(ntlm_hash)} {self.mark_pwned()}"
out = f"{self.domain}\\{self.username}:{process_secret(self.password)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
return True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
return False
except Exception as e:
self.logger.fail(f"{domain}\\{username}:{process_secret(ntlm_hash)} {e}")
except Exception:
error_msg = self.handle_mssql_reply()
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.password), error_msg if error_msg else ""))
return False
@reconnect_mssql
def hash_login(self, domain, username, ntlm_hash):
self.username = username
self.domain = domain
self.lmhash = ""
self.nthash = ""
if ntlm_hash.find(":") != -1:
self.lmhash, self.nthash = ntlm_hash.split(":")
else:
self.nthash = ntlm_hash
try:
res = self.conn.login(
None,
self.username,
"",
self.domain,
f"{self.lmhash}:{self.nthash}",
not self.args.local_auth,
)
if res is not True:
raise
self.check_if_admin()
out = f"{self.domain}\\{self.username}:{process_secret(self.nthash)} {self.mark_pwned()}"
self.logger.success(out)
if not self.args.local_auth:
add_user_bh(self.username, self.domain, self.logger, self.config)
if self.admin_privs:
add_user_bh(f"{self.hostname}$", self.domain, self.logger, self.config)
return True
except BrokenPipeError:
self.logger.fail("Broken Pipe Error while attempting to login")
return False
except Exception:
error_msg = self.handle_mssql_reply()
self.logger.fail("{}\\{}:{} {}".format(self.domain, self.username, process_secret(self.nthash), error_msg if error_msg else ""))
return False
def mssql_query(self):
@ -306,37 +294,30 @@ class mssql(connection):
else:
self.logger.fail("Unexpected output")
except Exception as e:
self.logger.exception(e)
self.logger.exception(f"Failed to excuted MSSQL query, reason: {e}")
return None
return raw_output
@requires_admin
def execute(self, payload=None, print_output=False):
def execute(self, payload=None, get_output=False):
if not payload and self.args.execute:
payload = self.args.execute
self.logger.info(f"Command to execute:\n{payload}")
if not self.args.no_output:
get_output = True
self.logger.info(f"Command to execute: {payload}")
try:
exec_method = MSSQLEXEC(self.conn)
raw_output = exec_method.execute(payload, print_output)
self.logger.info("Executed command via mssqlexec")
self.logger.debug(f"Raw output: {raw_output}")
exec_method = MSSQLEXEC(self.conn, self.logger)
raw_output = exec_method.execute(payload, get_output)
except Exception as e:
self.logger.exception(e)
return None
if hasattr(self, "server"):
self.server.track_host(self.host)
if self.args.execute or self.args.ps_execute:
self.logger.success("Executed command via mssqlexec")
if self.args.no_output:
self.logger.debug("Output set to disabled")
self.logger.fail(f"Execute command failed, error: {e!s}")
return False
else:
self.logger.success("Executed command via mssqlexec")
if raw_output:
for line in raw_output:
self.logger.highlight(line)
return raw_output
@requires_admin
@ -344,9 +325,8 @@ class mssql(connection):
self,
payload=None,
get_output=False,
methods=None,
force_ps32=False,
dont_obfs=True,
dont_obfs=False,
):
if not payload and self.args.ps_execute:
payload = self.args.ps_execute
@ -364,7 +344,7 @@ class mssql(connection):
try:
data = f.read()
self.logger.display(f"Size is {len(data)} bytes")
exec_method = MSSQLEXEC(self.conn)
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.put_file(data, self.args.put_file[1])
if exec_method.file_exists(self.args.put_file[1]):
self.logger.success("File has been uploaded on the remote machine")
@ -380,7 +360,7 @@ class mssql(connection):
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
try:
exec_method = MSSQLEXEC(self.conn)
exec_method = MSSQLEXEC(self.conn, self.logger)
exec_method.get_file(self.args.get_file[0], self.args.get_file[1])
self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"')
except Exception as e:
@ -394,13 +374,13 @@ class mssql(connection):
for keys in self.conn.replies:
for _i, key in enumerate(self.conn.replies[keys]):
if key["TokenType"] == TDS_ERROR_TOKEN:
error = f"ERROR({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}"
error_msg = f"({key['MsgText'].decode('utf-16le')} Please try again with or without '--local-auth')"
self.conn.lastError = SQLErrorException(f"ERROR: Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
self.logger.fail(error)
return error_msg
elif key["TokenType"] == TDS_INFO_TOKEN:
self.logger.display(f"INFO({key['ServerName'].decode('utf-16le')}): Line {key['LineNumber']:d}: {key['MsgText'].decode('utf-16le')}")
return f"({key['MsgText'].decode('utf-16le')})"
elif key["TokenType"] == TDS_LOGINACK_TOKEN:
self.logger.display(f"ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) ")
return f"(ACK: Result: {key['Interface']} - {key['ProgName'].decode('utf-16le')} ({key['MajorVer']:d}{key['MinorVer']:d} {key['BuildNumHi']:d}{key['BuildNumLow']:d}) )"
elif key["TokenType"] == TDS_ENVCHANGE_TOKEN and key["Type"] in (
TDS_ENVCHANGE_DATABASE,
TDS_ENVCHANGE_LANGUAGE,
@ -422,4 +402,4 @@ class mssql(connection):
_type = "PACKETSIZE"
else:
_type = f"{key['Type']:d}"
self.logger.display(f"ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')}")
return f"(ENVCHANGE({_type}): Old Value: {record['OldValue'].decode('utf-16le')}, New Value: {record['NewValue'].decode('utf-16le')})"

View File

@ -1,38 +1,38 @@
import binascii
from nxc.logger import nxc_logger
class MSSQLEXEC:
def __init__(self, connection):
def __init__(self, connection, logger):
self.mssql_conn = connection
self.outputBuffer = ""
self.logger = logger
self.outputBuffer = []
def execute(self, command, output=False):
command_output = []
try:
self.enable_xp_cmdshell()
except Exception as e:
nxc_logger.error(f"Error when attempting to enable x_cmdshell: {e}")
self.logger.error(f"Error when attempting to enable x_cmdshell: {e}")
try:
result = self.mssql_conn.sql_query(f"exec master..xp_cmdshell '{command}'")
nxc_logger.debug(f"SQL Query Result: {result}")
for row in result:
if row["output"] == "NULL":
continue
command_output.append(row["output"])
except Exception as e:
nxc_logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")
self.logger.error(f"Error when attempting to execute command via xp_cmdshell: {e}")
if output:
nxc_logger.debug("Output is enabled")
for row in command_output:
nxc_logger.debug(row)
# if len(self.outputBuffer):
try:
self.disable_xp_cmdshell()
except Exception as e:
nxc_logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
return command_output
self.logger.error(f"[OPSEC] Error when attempting to disable xp_cmdshell: {e}")
if output:
self.logger.debug(f"SQL Query Result: {result}")
for row in result:
if row["output"] == "NULL":
continue
self.outputBuffer.append(row["output"])
else:
self.logger.info("Output set to disabled")
return self.outputBuffer
def enable_xp_cmdshell(self):
self.mssql_conn.sql_query("exec master.dbo.sp_configure 'show advanced options',1;RECONFIGURE;exec master.dbo.sp_configure 'xp_cmdshell', 1;RECONFIGURE;")
@ -53,7 +53,7 @@ class MSSQLEXEC:
self.mssql_conn.sql_query(f"DECLARE @ob INT;EXEC sp_OACreate 'ADODB.Stream', @ob OUTPUT;EXEC sp_OASetProperty @ob, 'Type', 1;EXEC sp_OAMethod @ob, 'Open';EXEC sp_OAMethod @ob, 'Write', NULL, 0x{hexdata};EXEC sp_OAMethod @ob, 'SaveToFile', NULL, '{remote}', 2;EXEC sp_OAMethod @ob, 'Close';EXEC sp_OADestroy @ob;")
self.disable_ole()
except Exception as e:
nxc_logger.debug(f"Error uploading via mssqlexec: {e}")
self.logger.debug(f"Error uploading via mssqlexec: {e}")
def file_exists(self, remote):
try:
@ -71,4 +71,4 @@ class MSSQLEXEC:
f.write(binascii.unhexlify(data))
except Exception as e:
nxc_logger.debug(f"Error downloading via mssqlexec: {e}")
self.logger.debug(f"Error downloading via mssqlexec: {e}")

View File

@ -1,17 +1,13 @@
from argparse import _StoreTrueAction
def proto_args(parser, std_parser, module_parser):
mssql_parser = parser.add_parser("mssql", help="own stuff using MSSQL", parents=[std_parser, module_parser])
mssql_parser.add_argument("-H", "--hash", metavar="HASH", dest="hash", nargs="+", default=[], help="NTLM hash(es) or file(s) containing NTLM hashes")
mssql_parser.add_argument("--port", default=1433, type=int, metavar="PORT", help="MSSQL port (default: 1433)")
mssql_parser.add_argument("--mssql-timeout", help="SQL server connection timeout, default is %(default)s seconds", type=int, default=5)
mssql_parser.add_argument("-q", "--query", dest="mssql_query", metavar="QUERY", type=str, help="execute the specified query against the MSSQL DB")
no_smb_arg = mssql_parser.add_argument("--no-smb", action=get_conditional_action(_StoreTrueAction), make_required=[], help="No smb connection")
dgroup = mssql_parser.add_mutually_exclusive_group()
domain_arg = dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
dgroup.add_argument("-d", metavar="DOMAIN", dest="domain", type=str, help="domain name")
dgroup.add_argument("--local-auth", action="store_true", help="authenticate locally to each target")
no_smb_arg.make_required = [domain_arg]
cgroup = mssql_parser.add_argument_group("Command Execution", "options for executing commands")
cgroup.add_argument("--force-ps32", action="store_true", help="force the PowerShell command to run in a 32-bit process")
@ -25,22 +21,7 @@ def proto_args(parser, std_parser, module_parser):
psgroup.add_argument("--clear-obfscripts", action="store_true", help="Clear all cached obfuscated PowerShell scripts")
tgroup = mssql_parser.add_argument_group("Files", "Options for put and get remote files")
tgroup.add_argument("--put-file", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt")
tgroup.add_argument("--get-file", nargs=2, metavar="FILE", help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt")
tgroup.add_argument("--put-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Put a local file into remote target, ex: whoami.txt C:\\Windows\\Temp\\whoami.txt")
tgroup.add_argument("--get-file", nargs=2, metavar=("SRC_FILE", "DEST_FILE"), help="Get a remote file, ex: C:\\Windows\\Temp\\whoami.txt whoami.txt")
return parser
def get_conditional_action(baseAction):
class ConditionalAction(baseAction):
def __init__(self, option_strings, dest, **kwargs):
x = kwargs.pop("make_required", [])
super().__init__(option_strings, dest, **kwargs)
self.make_required = x
def __call__(self, parser, namespace, values, option_string=None):
for x in self.make_required:
x.required = True
super().__call__(parser, namespace, values, option_string)
return ConditionalAction